view-as-mime-type: fix bug in interactive declaration
[conkeror.git] / modules / download-manager.js
blob59b13650973fb28f0338e685dd7d1cc16049b425
1 /**
2  * (C) Copyright 2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2009 John Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 require("special-buffer.js");
10 require("mime-type-override.js");
11 require("minibuffer-read-mime-type.js");
13 var download_manager_service = Cc["@mozilla.org/download-manager;1"]
14     .getService(Ci.nsIDownloadManager);
16 var download_manager_builtin_ui = Components.classesByID["{7dfdf0d1-aff6-4a34-bad1-d0fe74601642}"]
17     .getService(Ci.nsIDownloadManagerUI);
19 /* This implements nsIHelperAppLauncherDialog interface. */
20 function download_helper () {}
21 download_helper.prototype = {
22     QueryInterface: generate_QI(Ci.nsIHelperAppLauncherDialog, Ci.nsIWebProgressListener2),
24     handle_show: function () {
25         var action_chosen = false;
27         var can_view_internally = this.frame != null &&
28                 can_override_mime_type_for_uri(this.launcher.source);
29         try {
30             this.panel = create_info_panel(this.window, "download-panel",
31                                            [["downloading", "Downloading:", this.launcher.source.spec],
32                                             ["mime-type", "Mime type:", this.launcher.MIMEInfo.MIMEType]]);
33             var action = yield this.window.minibuffer.read_single_character_option(
34                 $prompt = "Action to perform: (s: save; o: open; O: open URL; c: copy URL; " +
35                     (can_view_internally ? "i: view internally; t: view as text)" : ")"),
36                 $options = (can_view_internally ? ["s", "o", "O", "c", "i", "t"] : ["s", "o", "O", "c"]));
38             if (action == "s") {
39                 var suggested_path = suggest_save_path_from_file_name(this.launcher.suggestedFileName, this.buffer);
40                 var file = yield this.window.minibuffer.read_file_check_overwrite(
41                     $prompt = "Save to file:",
42                     $initial_value = suggested_path,
43                     $select);
44                 register_download(this.buffer, this.launcher.source);
45                 this.launcher.saveToDisk(file, false);
46                 action_chosen = true;
48             } else if (action == "o") {
49                 var cwd = make_file(with_current_buffer(this.buffer, function (I) I.local.cwd)).path;
50                 var mime_type = this.launcher.MIMEInfo.MIMEType;
51                 var suggested_action = get_mime_type_external_handler(mime_type);
52                 var command = yield this.window.minibuffer.read_shell_command(
53                     $initial_value = suggested_action,
54                     $cwd = cwd);
55                 var file = get_temporary_file(this.launcher.suggestedFileName);
56                 var info = register_download(this.buffer, this.launcher.source);
57                 info.temporary_status = DOWNLOAD_TEMPORARY_FOR_COMMAND;
58                 info.set_shell_command(command, cwd);
59                 this.launcher.saveToDisk(file, false);
60                 action_chosen = true;
61             } else if (action == "O") {
62                 action_chosen = true;
63                 this.abort(); // abort download
64                 let mime_type = this.launcher.MIMEInfo.MIMEType;
65                 let cwd = make_file(with_current_buffer(this.buffer || this.window.buffers.current,
66                                                         function (I) I.local.cwd)).path;
67                 let cmd = yield this.window.minibuffer.read_shell_command(
68                     $cwd = cwd,
69                     $initial_value = get_mime_type_external_handler(mime_type));
70                 shell_command_with_argument_blind(cmd, this.launcher.source.spec, $cwd = cwd);
71             } else if (action == "c") {
72                 action_chosen = true;
73                 this.abort(); // abort download
74                 let uri = this.launcher.source.spec;
75                 writeToClipboard(uri);
76                 this.window.minibuffer.message("Copied: " + uri);
77             } else /* if (action == "i" || action == "t") */ {
78                 let mime_type;
79                 if (action == "t")
80                     mime_type = "text/plain";
81                 else {
82                     let suggested_type = this.launcher.MIMEInfo.MIMEType;
83                     if (gecko_viewable_mime_type_list.indexOf(suggested_type) == -1)
84                         suggested_type = "text/plain";
85                     mime_type = yield this.window.minibuffer.read_gecko_viewable_mime_type(
86                         $prompt = "View internally as",
87                         $initial_value = suggested_type,
88                         $select);
89                 }
90                 action_chosen = true;
91                 this.abort(); // abort before reloading
93                 override_mime_type_for_next_load(this.launcher.source, mime_type);
94                 this.frame.location = this.launcher.source.spec; // reload
95             }
96         } catch (e) {
97             handle_interactive_error(this.window, e);
98         } finally {
99             if (!action_chosen)
100                 this.abort();
101             this.cleanup();
102         }
103     },
105     show : function (launcher, context, reason) {
106         this.launcher = launcher;
108         // Get associated buffer; if that fails (hopefully not), just get any window
109         var buffer = null;
110         var window = null;
111         var frame = null;
112         try {
113             frame = context.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal);
114             window = get_window_from_frame(frame.top);
115             if (window)
116                 buffer = get_buffer_from_frame(window, frame);
117         } catch (e) {
118             window = get_recent_conkeror_window();
120             if (window == null) {
121                 // FIXME: need to handle this case perhaps where no windows exist
122                 this.abort(); // for now, just cancel the download
123                 return;
124             }
125         }
127         this.frame = frame;
128         this.window = window;
129         this.buffer = buffer;
131         co_call(this.handle_show());
132     },
134     abort : function () {
135         const NS_BINDING_ABORTED = 0x804b0002;
136         this.launcher.cancel(NS_BINDING_ABORTED);
137     },
139     cleanup : function () {
140         if (this.panel)
141             this.panel.destroy();
142         this.panel = null;
143         this.launcher = null;
144         this.window = null;
145         this.buffer = null;
146         this.frame = null;
147     },
149     promptForSaveToFile : function (launcher, context, default_file, suggested_file_extension) {
150         return null;
151     }
155 var unmanaged_download_info_list = [];
156 var id_to_download_info = {};
158 // Import these constants for convenience
159 const DOWNLOAD_NOTSTARTED = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
160 const DOWNLOAD_DOWNLOADING = Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING;
161 const DOWNLOAD_FINISHED = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
162 const DOWNLOAD_FAILED = Ci.nsIDownloadManager.DOWNLOAD_FAILED;
163 const DOWNLOAD_CANCELED = Ci.nsIDownloadManager.DOWNLOAD_CANCELED;
164 const DOWNLOAD_PAUSED = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
165 const DOWNLOAD_QUEUED = Ci.nsIDownloadManager.DOWNLOAD_QUEUED;
166 const DOWNLOAD_BLOCKED = Ci.nsIDownloadManager.DOWNLOAD_BLOCKED;
167 const DOWNLOAD_SCANNING = Ci.nsIDownloadManager.DOWNLOAD_SCANNING;
170 const DOWNLOAD_NOT_TEMPORARY = 0;
171 const DOWNLOAD_TEMPORARY_FOR_ACTION = 1;
172 const DOWNLOAD_TEMPORARY_FOR_COMMAND = 2;
174 function download_info (source_buffer, mozilla_info, target_file) {
175     this.source_buffer = source_buffer;
176     this.target_file = target_file;
177     if (mozilla_info != null)
178         this.attach(mozilla_info);
180 download_info.prototype = {
181     attach : function (mozilla_info) {
182         if (!this.target_file)
183             this.__defineGetter__("target_file", function(){
184                     return this.mozilla_info.targetFile;
185                 });
186         else if (this.target_file.path != mozilla_info.targetFile.path)
187             throw interactive_error("Download target file unexpected.");
188         this.mozilla_info = mozilla_info;
189         id_to_download_info[mozilla_info.id] = this;
190         download_added_hook.run(this);
191     },
193     target_file : null,
195     shell_command : null,
197     shell_command_cwd : null,
199     temporary_status : DOWNLOAD_NOT_TEMPORARY,
201     action_description : null,
203     set_shell_command : function (str, cwd) {
204         this.shell_command = str;
205         this.shell_command_cwd = cwd;
206         if (this.mozilla_info)
207             download_shell_command_change_hook.run(this);
208     },
210     /**
211      * None of the following members may be used until attach is called
212      */
214     // Reflectors to properties of nsIDownload
215     get state () { return this.mozilla_info.state; },
216     get display_name () { return this.mozilla_info.displayName; },
217     get amount_transferred () { return this.mozilla_info.amountTransferred; },
218     get percent_complete () { return this.mozilla_info.percentComplete; },
219     get size () {
220         var s = this.mozilla_info.size;
221         /* nsIDownload.size is a PRUint64, and will have value
222          * LL_MAXUINT (2^64 - 1) to indicate an unknown size.  Because
223          * JavaScript only has a double numerical type, this value
224          * cannot be represented exactly, so 2^36 is used instead as the cutoff. */
225         if (s < 68719476736 /* 2^36 */)
226             return s;
227         return -1;
228     },
229     get source () { return this.mozilla_info.source; },
230     get start_time () { return this.mozilla_info.startTime; },
231     get speed () { return this.mozilla_info.speed; },
232     get MIME_info () { return this.mozilla_info.MIMEInfo; },
233     get MIME_type () {
234         if (this.MIME_info)
235             return this.MIME_info.MIMEType;
236         return null;
237     },
238     get id () { return this.mozilla_info.id; },
239     get referrer () { return this.mozilla_info.referrer; },
241     target_file_text : function() {
242         let target = this.target_file.path;
243         let display = this.display_name;
244         if (target.indexOf(display, target.length - display.length) == -1)
245             target += " (" + display + ")";
246         return target;
247     },
249     throw_if_removed : function () {
250         if (this.removed)
251             throw interactive_error("Download has already been removed from the download manager.");
252     },
254     throw_state_error : function () {
255         switch (this.state) {
256         case DOWNLOAD_DOWNLOADING:
257             throw interactive_error("Download is already in progress.");
258         case DOWNLOAD_FINISHED:
259             throw interactive_error("Download has already completed.");
260         case DOWNLOAD_FAILED:
261             throw interactive_error("Download has already failed.");
262         case DOWNLOAD_CANCELED:
263             throw interactive_error("Download has already been canceled.");
264         case DOWNLOAD_PAUSED:
265             throw interactive_error("Download has already been paused.");
266         case DOWNLOAD_QUEUED:
267             throw interactive_error("Download is queued.");
268         default:
269             throw new Error("Download has unexpected state: " + this.state);
270         }
271     },
273     // Download manager operations
274     cancel : function ()  {
275         this.throw_if_removed();
276         switch (this.state) {
277         case DOWNLOAD_DOWNLOADING:
278         case DOWNLOAD_PAUSED:
279         case DOWNLOAD_QUEUED:
280             try {
281                 download_manager_service.cancelDownload(this.id);
282             } catch (e) {
283                 throw interactive_error("Download cannot be canceled.");
284             }
285             break;
286         default:
287             this.throw_state_error();
288         }
289     },
291     retry : function () {
292         this.throw_if_removed();
293         switch (this.state) {
294         case DOWNLOAD_CANCELED:
295         case DOWNLOAD_FAILED:
296             try {
297                 download_manager_service.retryDownload(this.id);
298             } catch (e) {
299                 throw interactive_error("Download cannot be retried.");
300             }
301             break;
302         default:
303             this.throw_state_error();
304         }
305     },
307     resume : function () {
308         this.throw_if_removed();
309         switch (this.state) {
310         case DOWNLOAD_PAUSED:
311             try {
312                 download_manager_service.resumeDownload(this.id);
313             } catch (e) {
314                 throw interactive_error("Download cannot be resumed.");
315             }
316             break;
317         default:
318             this.throw_state_error();
319         }
320     },
322     pause : function () {
323         this.throw_if_removed();
324         switch (this.state) {
325         case DOWNLOAD_DOWNLOADING:
326         case DOWNLOAD_QUEUED:
327             try {
328                 download_manager_service.pauseDownload(this.id);
329             } catch (e) {
330                 throw interactive_error("Download cannot be paused.");
331             }
332             break;
333         default:
334             this.throw_state_error();
335         }
336     },
338     remove : function () {
339         this.throw_if_removed();
340         switch (this.state) {
341         case DOWNLOAD_FAILED:
342         case DOWNLOAD_CANCELED:
343         case DOWNLOAD_FINISHED:
344             try {
345                 download_manager_service.removeDownload(this.id);
346             } catch (e) {
347                 throw interactive_error("Download cannot be removed.");
348             }
349             break;
350         default:
351             throw interactive_error("Download is still in progress.");
352         }
353     },
355     delete_target : function () {
356         if (this.state != DOWNLOAD_FINISHED)
357             throw interactive_error("Download has not finished.");
358         try {
359             this.target_file.remove(false);
360         } catch (e) {
361             if ("result" in e) {
362                 switch (e.result) {
363                 case Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
364                     throw interactive_error("File has already been deleted.");
365                 case Cr.NS_ERROR_FILE_ACCESS_DENIED:
366                     throw interactive_error("Access denied");
367                 case Cr.NS_ERROR_FILE_DIR_NOT_EMPTY:
368                     throw interactive_error("Failed to delete file.");
369                 }
370             }
371             throw e;
372         }
373     }
376 var define_download_local_hook = simple_local_hook_definer();
378 // FIXME: add more parameters
379 function register_download (buffer, source_uri, target_file) {
380     var info = new download_info(buffer, null, target_file);
381     info.registered_time_stamp = Date.now();
382     info.registered_source_uri = source_uri;
383     unmanaged_download_info_list.push(info);
384     return info;
387 function match_registered_download (mozilla_info) {
388     let list = unmanaged_download_info_list;
389     let t = Date.now();
390     for (let i = 0; i < list.length; ++i) {
391         let x = list[i];
392         if (x.registered_source_uri == mozilla_info.source) {
393             list.splice(i, 1);
394             return x;
395         }
396         if (t - x.registered_time_stamp > download_info_max_queue_delay) {
397             list.splice(i, 1);
398             --i;
399             continue;
400         }
401     }
402     return null;
405 define_download_local_hook("download_added_hook");
406 define_download_local_hook("download_removed_hook");
407 define_download_local_hook("download_finished_hook");
408 define_download_local_hook("download_progress_change_hook");
409 define_download_local_hook("download_state_change_hook");
410 define_download_local_hook("download_shell_command_change_hook");
412 define_variable('delete_temporary_files_for_command', true,
413     'If this is set to true, temporary files downloaded to run a command '+
414     'on them will be deleted once the command completes. If not, the file '+
415     'will stay around forever unless deleted outside the browser.');
417 var download_info_max_queue_delay = 100;
419 var download_progress_listener = {
420     QueryInterface: generate_QI(Ci.nsIDownloadProgressListener),
422     onDownloadStateChange : function (state, download) {
423         var info = null;
424         /* FIXME: Determine if only new downloads will have this state
425          * as their previous state. */
427         dumpln("download state change: " + download.source.spec + ": " + state + ", " + download.state + ", " + download.id);
429         if (state == DOWNLOAD_NOTSTARTED) {
430             info = match_registered_download(download);
431             if (info == null) {
432                 info = new download_info(null, download);
433                 dumpln("error: encountered unknown new download");
434             } else {
435                 info.attach(download);
436             }
437         } else {
438             info = id_to_download_info[download.id];
439             if (info == null) {
440                 dumpln("Error: encountered unknown download");
442             } else {
443                 info.mozilla_info = download;
444                 download_state_change_hook.run(info);
445                 if (info.state == DOWNLOAD_FINISHED) {
446                     download_finished_hook.run(info);
448                     if (info.shell_command != null) {
449                         info.running_shell_command = true;
450                         co_call(function () {
451                             try {
452                                 yield shell_command_with_argument(info.shell_command,
453                                                                   info.target_file.path,
454                                                                   $cwd = info.shell_command_cwd);
455                             } finally  {
456                                 if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
457                                     if(delete_temporary_files_for_command) {
458                                         info.target_file.remove(false /* not recursive */);
459                                     }
460                                 info.running_shell_command = false;
461                                 download_shell_command_change_hook.run(info);
462                             }
463                         }());
464                         download_shell_command_change_hook.run(info);
465                     }
466                 }
467             }
468         }
469     },
471     onProgressChange : function (progress, request, cur_self_progress, max_self_progress,
472                                  cur_total_progress, max_total_progress,
473                                  download) {
474         var info = id_to_download_info[download.id];
475         if (info == null) {
476             dumpln("error: encountered unknown download in progress change");
477             return;
478         }
479         info.mozilla_info = download;
480         download_progress_change_hook.run(info);
481         //dumpln("download progress change: " + download.source.spec + ": " + cur_self_progress + "/" + max_self_progress + " "
482         // + cur_total_progress + "/" + max_total_progress + ", " + download.state + ", " + download.id);
483     },
485     onSecurityChange : function (progress, request, state, download) {
486     },
488     onStateChange : function (progress, request, state_flags, status, download) {
489     }
492 var download_observer = {
493     observe : function (subject, topic, data) {
494         switch(topic) {
495         case "download-manager-remove-download":
496             var ids = [];
497             if (!subject) {
498                 // Remove all downloads
499                 for (let i in id_to_download_info)
500                     ids.push(i);
501             } else {
502                 let id = subject.QueryInterface(Ci.nsISupportsPRUint32);
503                 /* FIXME: determine if this should really be an error */
504                 if (!(id in id_to_download_info)) {
505                     dumpln("Error: download-manager-remove-download event received for unknown download: " + id);
506                 } else
507                     ids.push(id);
508             }
509             for each (let i in ids) {
510                 dumpln("deleting download: " + i);
511                 let d = id_to_download_info[i];
512                 d.removed = true;
513                 download_removed_hook.run(d);
514                 delete id_to_download_info[i];
515             }
516             break;
517         }
518     }
520 observer_service.addObserver(download_observer, "download-manager-remove-download", false);
522 download_manager_service.addListener(download_progress_listener);
524 function pretty_print_file_size (val) {
525     const GIBI = 1073741824; /* 2^30 */
526     const MEBI = 1048576; /* 2^20 */
527     const KIBI = 1024; /* 2^10 */
528     var suffix, div;
529     if (val < KIBI) {
530         div = 1;
531         suffix = "B";
532     }
533     else if (val < MEBI) {
534         suffix = "KiB";
535         div = KIBI;
536     } else if (val < GIBI) {
537         suffix = "MiB";
538         div = MEBI;
539     } else {
540         suffix = "GiB";
541         div = GIBI;
542     }
543     val = val / div;
544     var precision = 2;
545     if (val > 10)
546         precision = 1;
547     if (val > 100)
548         precision = 0;
549     return [val.toFixed(precision), suffix];
552 function pretty_print_time (val) {
553     val = Math.round(val);
554     var seconds = val % 60;
556     val = Math.floor(val / 60);
558     var minutes = val % 60;
560     var hours = Math.floor(val / 60);
562     var parts = [];
564     if (hours > 1)
565         parts.push(hours + " hours");
566     else if (hours == 1)
567         parts.push("1 hour");
569     if (minutes > 1)
570         parts.push(minutes + " minutes");
571     else if (minutes == 1)
572         parts.push("1 minute");
574     if (minutes <= 1 && hours == 0) {
575         if (seconds != 1)
576             parts.push(seconds + " seconds");
577         else
578             parts.push("1 second");
579     }
581     return parts.join(", ");
584 define_variable("download_buffer_min_update_interval", 2000,
585     "Minimum interval (in milliseconds) between updates in download progress buffers.\n" +
586     "Lowering this interval will increase the promptness of the progress display at " +
587     "the cost of using additional processor time.");
589 define_keywords("$info");
590 function download_buffer (window, element) {
591     this.constructor_begin();
592     keywords(arguments);
593     special_buffer.call(this, window, element, forward_keywords(arguments));
594     this.info = arguments.$info;
595     this.local.cwd = this.info.mozilla_info.targetFile.parent.path;
596     this.description = this.info.mozilla_info.source.spec;
597     this.keymap = download_buffer_keymap;
598     this.update_title();
600     this.progress_change_handler_fn = method_caller(this, this.handle_progress_change);
601     add_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
602     add_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
603     this.command_change_handler_fn = method_caller(this, this.update_command_field);
604     add_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
605     this.constructor_end();
607 download_buffer.prototype = {
608     __proto__: special_buffer.prototype,
610     handle_kill : function () {
611         this.__proto__.__proto__.handle_kill.call(this);
612         remove_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
613         remove_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
614         remove_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
616         // Remove all node references
617         delete this.status_textnode;
618         delete this.target_file_node;
619         delete this.transferred_div_node;
620         delete this.transferred_textnode;
621         delete this.progress_container_node;
622         delete this.progress_bar_node;
623         delete this.percent_textnode;
624         delete this.time_textnode;
625         delete this.command_div_node;
626         delete this.command_label_textnode;
627         delete this.command_textnode;
628     },
630     update_title : function () {
631         // FIXME: do this properly
632         var new_title;
633         var info = this.info;
634         var append_transfer_info = false;
635         var append_speed_info = true;
636         var label = null;
637         switch(info.state) {
638         case DOWNLOAD_DOWNLOADING:
639             label = "Downloading";
640             append_transfer_info = true;
641             break;
642         case DOWNLOAD_FINISHED:
643             label = "Download complete";
644             break;
645         case DOWNLOAD_FAILED:
646             label = "Download failed";
647             append_transfer_info = true;
648             append_speed_info = false;
649             break;
650         case DOWNLOAD_CANCELED:
651             label = "Download canceled";
652             append_transfer_info = true;
653             append_speed_info = false;
654             break;
655         case DOWNLOAD_PAUSED:
656             label = "Download paused";
657             append_transfer_info = true;
658             append_speed_info = false;
659             break;
660         case DOWNLOAD_QUEUED:
661         default:
662             label = "Download queued";
663             break;
664         }
666         if (append_transfer_info) {
667             if (append_speed_info)
668                 new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
669             else
670                 new_title = label + ": ";
671             var trans = pretty_print_file_size(info.amount_transferred);
672             if (info.size >= 0) {
673                 var total = pretty_print_file_size(info.size);
674                 if (trans[1] == total[1])
675                     new_title += trans[0] + "/" + total[0] + " " + total[1];
676                 else
677                     new_title += trans.join(" ") + "/" + total.join(" ");
678             } else
679                 new_title += trans.join(" ");
680             if (info.percent_complete >= 0)
681                 new_title += " (" + info.percent_complete + "%)";
682         } else
683             new_title = label;
684         if (new_title != this.title) {
685             this.title = new_title;
686             return true;
687         }
688         return false;
689     },
691     handle_progress_change : function () {
692         var cur_time = Date.now();
693         if (this.last_update == null ||
694             (cur_time - this.last_update) > download_buffer_min_update_interval ||
695             this.info.state != this.previous_state) {
697             if (this.update_title())
698                 buffer_title_change_hook.run(this);
700             if (this.generated) {
701                 this.update_fields();
702             }
703             this.previous_status = this.info.status;
704             this.last_update = cur_time;
705         }
706     },
708     generate : function () {
709         var d = this.document;
710         var g = new dom_generator(d, XHTML_NS);
712         /* Warning: If any additional node references are saved in
713          * this function, appropriate code to delete the saved
714          * properties must be added to handle_kill. */
716         var info = this.info;
718         d.body.setAttribute("class", "download-buffer");
720         g.add_stylesheet("chrome://conkeror-gui/content/downloads.css");
722         var row, cell;
723         var table = g.element("table", d.body);
725         row = g.element("tr", table, "class", "download-info", "id", "download-source");
726         cell = g.element("td", row, "class", "download-label");
727         this.status_textnode = g.text("", cell);
728         cell = g.element("td", row, "class", "download-value");
729         g.text(info.source.spec, cell);
731         row = g.element("tr", table, "class", "download-info", "id", "download-target");
732         cell = g.element("td", row, "class", "download-label");
733         var target_label;
734         if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
735             target_label = "Temp. file:";
736         else
737             target_label = "Target:";
738         g.text(target_label, cell);
739         cell = g.element("td", row, "class", "download-value");
740         this.target_file_node = g.text("", cell);
742         row = g.element("tr", table, "class", "download-info", "id", "download-mime-type");
743         cell = g.element("td", row, "class", "download-label");
744         g.text("MIME type:", cell);
745         cell = g.element("td", row, "class", "download-value");
746         g.text(info.MIME_type || "unknown", cell);
748         this.transferred_div_node = row =
749             g.element("tr", table, "class", "download-info", "id", "download-transferred");
750         cell = g.element("td", row, "class", "download-label");
751         g.text("Transferred:", cell);
752         cell = g.element("td", row, "class", "download-value");
753         var sub_item = g.element("div", cell);
754         this.transferred_textnode = g.text("", sub_item);
755         sub_item = g.element("div", cell, "id", "download-percent");
756         this.percent_textnode = g.text("", sub_item);
757         this.progress_container_node = sub_item = g.element("div", cell, "id", "download-progress-container");
758         this.progress_bar_node = g.element("div", sub_item, "id", "download-progress-bar");
760         row = g.element("tr", table, "class", "download-info", "id", "download-time");
761         cell = g.element("td", row, "class", "download-label");
762         g.text("Time:", cell);
763         cell = g.element("td", row, "class", "download-value");
764         this.time_textnode = g.text("", cell);
766         if (info.action_description != null) {
767             row = g.element("tr", table, "class", "download-info", "id", "download-action");
768             cell = g.element("div", row, "class", "download-label");
769             g.text("Action:", cell);
770             cell = g.element("div", row, "class", "download-value");
771             g.text(info.action_description, cell);
772        }
774         this.command_div_node = row = g.element("tr", table, "class", "download-info", "id", "download-command");
775         cell = g.element("td", row, "class", "download-label");
776         this.command_label_textnode = g.text("Run command:", cell);
777         cell = g.element("td", row, "class", "download-value");
778         this.command_textnode = g.text("", cell);
781         this.update_fields();
782         this.update_command_field();
783     },
785     update_fields : function () {
786         if (!this.generated)
787             return;
788         var info = this.info;
789         var label = null;
790         switch(info.state) {
791         case DOWNLOAD_DOWNLOADING:
792             label = "Downloading";
793             break;
794         case DOWNLOAD_FINISHED:
795             label = "Completed";
796             break;
797         case DOWNLOAD_FAILED:
798             label = "Failed";
799             break;
800         case DOWNLOAD_CANCELED:
801             label = "Canceled";
802             break;
803         case DOWNLOAD_PAUSED:
804             label = "Paused";
805             break;
806         case DOWNLOAD_QUEUED:
807         default:
808             label = "Queued";
809             break;
810         }
811         this.status_textnode.nodeValue = label + ":";
812         this.target_file_node.nodeValue = info.target_file_text();
813         this.update_time_field();
815         var tran_text = "";
816         if (info.state == DOWNLOAD_FINISHED)
817             tran_text = pretty_print_file_size(info.size).join(" ");
818         else {
819             var trans = pretty_print_file_size(info.amount_transferred);
820             if (info.size >= 0) {
821                 var total = pretty_print_file_size(info.size);
822                 if (trans[1] == total[1])
823                     tran_text += trans[0] + "/" + total[0] + " " + total[1];
824                 else
825                     tran_text += trans.join(" ") + "/" + total.join(" ");
826             } else
827                 tran_text += trans.join(" ");
828         }
829         this.transferred_textnode.nodeValue = tran_text;
830         if (info.percent_complete >= 0) {
831             this.progress_container_node.style.display = "";
832             this.percent_textnode.nodeValue = info.percent_complete + "%";
833             this.progress_bar_node.style.width = info.percent_complete + "%";
834         } else {
835             this.percent_textnode.nodeValue = "";
836             this.progress_container_node.style.display = "none";
837         }
839         this.update_command_field();
840     },
842     update_time_field : function () {
843         var info = this.info;
844         var elapsed_text = pretty_print_time((Date.now() - info.start_time / 1000) / 1000) + " elapsed";
845         var text = "";
846         if (info.state == DOWNLOAD_DOWNLOADING) {
847             text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
848         }
849         if (info.state == DOWNLOAD_DOWNLOADING &&
850             info.size >= 0 &&
851             info.speed > 0) {
852             let remaining = (info.size - info.amount_transferred) / info.speed;
853             text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
854         } else {
855             text = elapsed_text;
856         }
857         this.time_textnode.nodeValue = text;
858     },
860     update_command_field : function () {
861         if (!this.generated)
862             return;
863         if (this.info.shell_command != null) {
864             this.command_div_node.style.display = "";
865             var label;
866             if (this.info.running_shell_command)
867                 label = "Running:";
868             else if (this.info.state == DOWNLOAD_FINISHED)
869                 label = "Ran command:";
870             else
871                 label = "Run command:";
872             this.command_label_textnode.nodeValue = label;
873             this.command_textnode.nodeValue = this.info.shell_command;
874         } else {
875             this.command_div_node.style.display = "none";
876         }
877     }
880 function download_cancel (buffer) {
881     check_buffer(buffer, download_buffer);
882     var info = buffer.info;
883     info.cancel();
884     buffer.window.minibuffer.message("Download canceled");
886 interactive("download-cancel",
887             "Cancel the current download.\n" +
888             "The download can later be retried using the `download-retry' command, but any " +
889             "data already transferred will be lost.",
890             function (I) {
891                 let result = yield I.window.minibuffer.read_single_character_option(
892                     $prompt = "Cancel this download? (y/n)",
893                     $options = ["y", "n"]);
894                 if (result == "y")
895                     download_cancel(I.buffer);
896             });
898 function download_retry (buffer) {
899     check_buffer(buffer, download_buffer);
900     var info = buffer.info;
901     info.retry();
902     buffer.window.minibuffer.message("Download retried");
904 interactive("download-retry",
905             "Retry a failed or canceled download.\n" +
906             "This command can be used to retry a download that failed or was canceled using " +
907             "the `download-cancel' command.  The download will begin from the start again.",
908             function (I) {download_retry(I.buffer);});
910 function download_pause (buffer) {
911     check_buffer(buffer, download_buffer);
912     buffer.info.pause();
913     buffer.window.minibuffer.message("Download paused");
915 interactive("download-pause",
916             "Pause the current download.\n" +
917             "The download can later be resumed using the `download-resume' command.  The " +
918             "data already transferred will not be lost.",
919             function (I) {download_pause(I.buffer);});
921 function download_resume (buffer) {
922     check_buffer(buffer, download_buffer);
923     buffer.info.resume();
924     buffer.window.minibuffer.message("Download resumed");
926 interactive("download-resume",
927             "Resume the current download.\n" +
928             "This command can be used to resume a download paused using the `download-pause' command.",
929             function (I) { download_resume(I.buffer); });
931 function download_remove (buffer) {
932     check_buffer(buffer, download_buffer);
933     buffer.info.remove();
934     buffer.window.minibuffer.message("Download removed");
936 interactive("download-remove",
937             "Remove the current download from the download manager.\n" +
938             "This command can only be used on inactive (paused, canceled, "+
939             "completed, or failed) downloads.",
940             function (I) {download_remove(I.buffer);});
942 function download_retry_or_resume (buffer) {
943     check_buffer(buffer, download_buffer);
944     var info = buffer.info;
945     if (info.state == DOWNLOAD_PAUSED)
946         download_resume(buffer);
947     else
948         download_retry(buffer);
950 interactive("download-retry-or-resume",
951             "Retry or resume the current download.\n" +
952             "This command can be used to resume a download paused using the `download-pause' " +
953             "command or canceled using the `download-cancel' command.",
954             function (I) {download_retry_or_resume(I.buffer);});
956 function download_pause_or_resume (buffer) {
957     check_buffer(buffer, download_buffer);
958     var info = buffer.info;
959     if (info.state == DOWNLOAD_PAUSED)
960         download_resume(buffer);
961     else
962         download_pause(buffer);
964 interactive("download-pause-or-resume",
965             "Pause or resume the current download.\n" +
966             "This command toggles the paused state of the current download.",
967             function (I) {download_pause_or_resume(I.buffer);});
969 function download_delete_target (buffer) {
970     check_buffer(buffer, download_buffer);
971     var info = buffer.info;
972     info.delete_target();
973     buffer.window.minibuffer.message("Deleted file: " + info.target_file.path);
975 interactive("download-delete-target",
976             "Delete the target file of the current download.\n"  +
977             "This command can only be used if the download has finished successfully.",
978             function (I) {download_delete_target(I.buffer);});
980 function download_shell_command (buffer, cwd, cmd) {
981     check_buffer(buffer, download_buffer);
982     var info = buffer.info;
983     if (info.state == DOWNLOAD_FINISHED) {
984         shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd);
985         return;
986     }
987     if (info.state != DOWNLOAD_DOWNLOADING && info.state != DOWNLOAD_PAUSED && info.state != DOWNLOAD_QUEUED)
988         info.throw_state_error();
989     if (cmd == null || cmd.length == 0)
990         info.set_shell_command(null, cwd);
991     else
992         info.set_shell_command(cmd, cwd);
993     buffer.window.minibuffer.message("Queued shell command: " + cmd);
995 interactive("download-shell-command",
996             "Run a shell command on the target file of the current download.\n" +
997             "If the download is still in progress, the shell command will be queued " +
998             "to run when the download finishes.",
999             function (I) {
1000                 var buffer = check_buffer(I.buffer, download_buffer);
1001                 var cwd = buffer.info.shell_command_cwd || I.local.cwd;
1002                 var cmd = yield I.minibuffer.read_shell_command(
1003                     $cwd = cwd,
1004                     $initial_value = buffer.info.shell_command ||
1005                         get_mime_type_external_handler(buffer.info.MIME_type));
1006                 download_shell_command(buffer, cwd, cmd);
1007             });
1009 function download_manager_ui () {}
1010 download_manager_ui.prototype = {
1011     QueryInterface : XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
1013     getAttention : function () {},
1014     show : function () {},
1015     visible : false
1019 function download_manager_show_builtin_ui (window) {
1020     download_manager_builtin_ui.show(window);
1022 interactive("download-manager-show-builtin-ui",
1023             "Show the built-in (Firefox-style) download manager user interface.",
1024             function (I) {download_manager_show_builtin_ui(I.window);});
1028 define_variable("download_temporary_file_open_buffer_delay", 500,
1029     "Delay (in milliseconds) before a download buffer is opened for temporary downloads.\n" +
1030     "This variable takes effect only if `open_download_buffer_automatically' is in " +
1031     "`download_added_hook', as it is by default.");
1033 define_variable("download_buffer_automatic_open_target",
1034                 [OPEN_NEW_WINDOW, OPEN_NEW_BUFFER_BACKGROUND],
1035     "Target(s) for download buffers created by "+
1036     "`open_download_buffer_automatically' and `download-open'.\n"+
1037     "It can be a single target or an array of two targets.  When it is an "+
1038     "array, the `download-open' command will use the second target when "+
1039     "called with universal-argument.");
1042 function open_download_buffer_automatically (info, target) {
1043     var buf = info.source_buffer;
1044     if (target == null) {
1045         if (typeof(download_buffer_automatic_open_target) == "object")
1046             target = download_buffer_automatic_open_target[0];
1047         else
1048             target = download_buffer_automatic_open_target;
1049     }
1050     if (buf == null)
1051         target = OPEN_NEW_WINDOW;
1052     if (info.temporary_status == DOWNLOAD_NOT_TEMPORARY ||
1053         !(download_temporary_file_open_buffer_delay > 0))
1054         create_buffer(buf.window, buffer_creator(download_buffer, $info = info), target);
1055     else {
1056         var timer = null;
1057         function finish () {
1058             timer.cancel();
1059         }
1060         add_hook.call(info, "download_finished_hook", finish);
1061         timer = call_after_timeout(function () {
1062                 remove_hook.call(info, "download_finished_hook", finish);
1063                 create_buffer(buf.window, buffer_creator(download_buffer, $info = info), target);
1064             }, download_temporary_file_open_buffer_delay);
1065     }
1067 add_hook("download_added_hook", open_download_buffer_automatically);
1071  * Download-show
1072  */ 
1074 minibuffer_auto_complete_preferences.download = true;
1076 minibuffer.prototype.read_download = function () {
1077     keywords(arguments,
1078              $prompt = "Download",
1079              $completer = all_word_completer(
1080                  $completions = function (visitor) {
1081                      var dls = download_manager_service.activeDownloads;
1082                      while (dls.hasMoreElements()) {
1083                          let dl = dls.getNext();
1084                          visitor(id_to_download_info[dl.id]);
1085                      }
1086                  },
1087                  $get_string = function (x) x.display_name,
1088                  $get_description = function (x) x.source.spec,
1089                  $get_value = function (x) x),
1090              $auto_complete = "download",
1091              $auto_complete_initial = true,
1092              $match_required = true);
1093     var result = yield this.read(forward_keywords(arguments));
1094     yield co_return(result);
1097 interactive("download-show",
1098     "Prompt for an ongoing download and open a download buffer showing "+
1099     "its progress.  When called with universal argument, the second "+
1100     "target from `download_buffer_automatic_open_target' will be used.",
1101     function (I) {
1102         var target = null;
1103         if (I.P && typeof(download_buffer_automatic_open_target) == "object")
1104             target = download_buffer_automatic_open_target[1];
1105         open_download_buffer_automatically(
1106             (yield I.minibuffer.read_download($prompt = "Show download:")),
1107             target);
1108     });