2 * (C) Copyright 2008 Jeremy Maitin-Shepard
3 * (C) Copyright 2009 John Foerch
5 * Use, modification, and distribution are subject to the terms specified in the
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);
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"]));
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,
44 register_download(this.buffer, this.launcher.source);
45 this.launcher.saveToDisk(file, false);
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,
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);
61 } else if (action == "O") {
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(
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") {
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") */ {
80 mime_type = "text/plain";
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,
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
97 handle_interactive_error(this.window, e);
105 show : function (launcher, context, reason) {
106 this.launcher = launcher;
108 // Get associated buffer; if that fails (hopefully not), just get any window
113 frame = context.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal);
114 window = get_window_from_frame(frame.top);
116 buffer = get_buffer_from_frame(window, frame);
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
128 this.window = window;
129 this.buffer = buffer;
131 co_call(this.handle_show());
134 abort : function () {
135 const NS_BINDING_ABORTED = 0x804b0002;
136 this.launcher.cancel(NS_BINDING_ABORTED);
139 cleanup : function () {
141 this.panel.destroy();
143 this.launcher = null;
149 promptForSaveToFile : function (launcher, context, default_file, suggested_file_extension) {
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;
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);
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);
211 * None of the following members may be used until attach is called
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; },
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 */)
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; },
235 return this.MIME_info.MIMEType;
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 + ")";
249 throw_if_removed : function () {
251 throw interactive_error("Download has already been removed from the download manager.");
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.");
269 throw new Error("Download has unexpected state: " + this.state);
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:
281 download_manager_service.cancelDownload(this.id);
283 throw interactive_error("Download cannot be canceled.");
287 this.throw_state_error();
291 retry : function () {
292 this.throw_if_removed();
293 switch (this.state) {
294 case DOWNLOAD_CANCELED:
295 case DOWNLOAD_FAILED:
297 download_manager_service.retryDownload(this.id);
299 throw interactive_error("Download cannot be retried.");
303 this.throw_state_error();
307 resume : function () {
308 this.throw_if_removed();
309 switch (this.state) {
310 case DOWNLOAD_PAUSED:
312 download_manager_service.resumeDownload(this.id);
314 throw interactive_error("Download cannot be resumed.");
318 this.throw_state_error();
322 pause : function () {
323 this.throw_if_removed();
324 switch (this.state) {
325 case DOWNLOAD_DOWNLOADING:
326 case DOWNLOAD_QUEUED:
328 download_manager_service.pauseDownload(this.id);
330 throw interactive_error("Download cannot be paused.");
334 this.throw_state_error();
338 remove : function () {
339 this.throw_if_removed();
340 switch (this.state) {
341 case DOWNLOAD_FAILED:
342 case DOWNLOAD_CANCELED:
343 case DOWNLOAD_FINISHED:
345 download_manager_service.removeDownload(this.id);
347 throw interactive_error("Download cannot be removed.");
351 throw interactive_error("Download is still in progress.");
355 delete_target : function () {
356 if (this.state != DOWNLOAD_FINISHED)
357 throw interactive_error("Download has not finished.");
359 this.target_file.remove(false);
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.");
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);
387 function match_registered_download (mozilla_info) {
388 let list = unmanaged_download_info_list;
390 for (let i = 0; i < list.length; ++i) {
392 if (x.registered_source_uri == mozilla_info.source) {
396 if (t - x.registered_time_stamp > download_info_max_queue_delay) {
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) {
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);
432 info = new download_info(null, download);
433 dumpln("error: encountered unknown new download");
435 info.attach(download);
438 info = id_to_download_info[download.id];
440 dumpln("Error: encountered unknown download");
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 () {
452 yield shell_command_with_argument(info.shell_command,
453 info.target_file.path,
454 $cwd = info.shell_command_cwd);
456 if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
457 if(delete_temporary_files_for_command) {
458 info.target_file.remove(false /* not recursive */);
460 info.running_shell_command = false;
461 download_shell_command_change_hook.run(info);
464 download_shell_command_change_hook.run(info);
471 onProgressChange : function (progress, request, cur_self_progress, max_self_progress,
472 cur_total_progress, max_total_progress,
474 var info = id_to_download_info[download.id];
476 dumpln("error: encountered unknown download in progress change");
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);
485 onSecurityChange : function (progress, request, state, download) {
488 onStateChange : function (progress, request, state_flags, status, download) {
492 var download_observer = {
493 observe : function (subject, topic, data) {
495 case "download-manager-remove-download":
498 // Remove all downloads
499 for (let i in id_to_download_info)
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);
509 for each (let i in ids) {
510 dumpln("deleting download: " + i);
511 let d = id_to_download_info[i];
513 download_removed_hook.run(d);
514 delete id_to_download_info[i];
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 */
533 else if (val < MEBI) {
536 } else if (val < GIBI) {
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);
565 parts.push(hours + " hours");
567 parts.push("1 hour");
570 parts.push(minutes + " minutes");
571 else if (minutes == 1)
572 parts.push("1 minute");
574 if (minutes <= 1 && hours == 0) {
576 parts.push(seconds + " seconds");
578 parts.push("1 second");
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();
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;
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;
630 update_title : function () {
631 // FIXME: do this properly
633 var info = this.info;
634 var append_transfer_info = false;
635 var append_speed_info = true;
638 case DOWNLOAD_DOWNLOADING:
639 label = "Downloading";
640 append_transfer_info = true;
642 case DOWNLOAD_FINISHED:
643 label = "Download complete";
645 case DOWNLOAD_FAILED:
646 label = "Download failed";
647 append_transfer_info = true;
648 append_speed_info = false;
650 case DOWNLOAD_CANCELED:
651 label = "Download canceled";
652 append_transfer_info = true;
653 append_speed_info = false;
655 case DOWNLOAD_PAUSED:
656 label = "Download paused";
657 append_transfer_info = true;
658 append_speed_info = false;
660 case DOWNLOAD_QUEUED:
662 label = "Download queued";
666 if (append_transfer_info) {
667 if (append_speed_info)
668 new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
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];
677 new_title += trans.join(" ") + "/" + total.join(" ");
679 new_title += trans.join(" ");
680 if (info.percent_complete >= 0)
681 new_title += " (" + info.percent_complete + "%)";
684 if (new_title != this.title) {
685 this.title = new_title;
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();
703 this.previous_status = this.info.status;
704 this.last_update = cur_time;
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");
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");
734 if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
735 target_label = "Temp. file:";
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);
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();
785 update_fields : function () {
788 var info = this.info;
791 case DOWNLOAD_DOWNLOADING:
792 label = "Downloading";
794 case DOWNLOAD_FINISHED:
797 case DOWNLOAD_FAILED:
800 case DOWNLOAD_CANCELED:
803 case DOWNLOAD_PAUSED:
806 case DOWNLOAD_QUEUED:
811 this.status_textnode.nodeValue = label + ":";
812 this.target_file_node.nodeValue = info.target_file_text();
813 this.update_time_field();
816 if (info.state == DOWNLOAD_FINISHED)
817 tran_text = pretty_print_file_size(info.size).join(" ");
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];
825 tran_text += trans.join(" ") + "/" + total.join(" ");
827 tran_text += trans.join(" ");
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 + "%";
835 this.percent_textnode.nodeValue = "";
836 this.progress_container_node.style.display = "none";
839 this.update_command_field();
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";
846 if (info.state == DOWNLOAD_DOWNLOADING) {
847 text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
849 if (info.state == DOWNLOAD_DOWNLOADING &&
852 let remaining = (info.size - info.amount_transferred) / info.speed;
853 text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
857 this.time_textnode.nodeValue = text;
860 update_command_field : function () {
863 if (this.info.shell_command != null) {
864 this.command_div_node.style.display = "";
866 if (this.info.running_shell_command)
868 else if (this.info.state == DOWNLOAD_FINISHED)
869 label = "Ran command:";
871 label = "Run command:";
872 this.command_label_textnode.nodeValue = label;
873 this.command_textnode.nodeValue = this.info.shell_command;
875 this.command_div_node.style.display = "none";
880 function download_cancel (buffer) {
881 check_buffer(buffer, download_buffer);
882 var info = buffer.info;
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.",
891 let result = yield I.window.minibuffer.read_single_character_option(
892 $prompt = "Cancel this download? (y/n)",
893 $options = ["y", "n"]);
895 download_cancel(I.buffer);
898 function download_retry (buffer) {
899 check_buffer(buffer, download_buffer);
900 var info = buffer.info;
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);
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);
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);
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);
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);
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.",
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(
1004 $initial_value = buffer.info.shell_command ||
1005 get_mime_type_external_handler(buffer.info.MIME_type));
1006 download_shell_command(buffer, cwd, cmd);
1009 function download_manager_ui () {}
1010 download_manager_ui.prototype = {
1011 QueryInterface : XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
1013 getAttention : function () {},
1014 show : function () {},
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];
1048 target = download_buffer_automatic_open_target;
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);
1057 function finish () {
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);
1067 add_hook("download_added_hook", open_download_buffer_automatically);
1074 minibuffer_auto_complete_preferences.download = true;
1076 minibuffer.prototype.read_download = function () {
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]);
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.",
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:")),