• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// TODO(jhawkins): Use hidden instead of showInline* and display:none.
6
7/**
8 * Sets the display style of a node.
9 * @param {!Element} node The target element to show or hide.
10 * @param {boolean} isShow Should the target element be visible.
11 */
12function showInline(node, isShow) {
13  node.style.display = isShow ? 'inline' : 'none';
14}
15
16/**
17 * Sets the display style of a node.
18 * @param {!Element} node The target element to show or hide.
19 * @param {boolean} isShow Should the target element be visible.
20 */
21function showInlineBlock(node, isShow) {
22  node.style.display = isShow ? 'inline-block' : 'none';
23}
24
25/**
26 * Creates a link with a specified onclick handler and content.
27 * @param {function()} onclick The onclick handler.
28 * @param {string} value The link text.
29 * @return {Element} The created link element.
30 */
31function createLink(onclick, value) {
32  var link = document.createElement('a');
33  link.onclick = onclick;
34  link.href = '#';
35  link.textContent = value;
36  link.oncontextmenu = function() { return false; };
37  return link;
38}
39
40/**
41 * Creates a button with a specified onclick handler and content.
42 * @param {function()} onclick The onclick handler.
43 * @param {string} value The button text.
44 * @return {Element} The created button.
45 */
46function createButton(onclick, value) {
47  var button = document.createElement('input');
48  button.type = 'button';
49  button.value = value;
50  button.onclick = onclick;
51  return button;
52}
53
54///////////////////////////////////////////////////////////////////////////////
55// Downloads
56/**
57 * Class to hold all the information about the visible downloads.
58 * @constructor
59 */
60function Downloads() {
61  this.downloads_ = {};
62  this.node_ = $('downloads-display');
63  this.summary_ = $('downloads-summary-text');
64  this.searchText_ = '';
65
66  // Keep track of the dates of the newest and oldest downloads so that we
67  // know where to insert them.
68  this.newestTime_ = -1;
69
70  // Icon load request queue.
71  this.iconLoadQueue_ = [];
72  this.isIconLoading_ = false;
73}
74
75/**
76 * Called when a download has been updated or added.
77 * @param {Object} download A backend download object (see downloads_ui.cc)
78 */
79Downloads.prototype.updated = function(download) {
80  var id = download.id;
81  if (!!this.downloads_[id]) {
82    this.downloads_[id].update(download);
83  } else {
84    this.downloads_[id] = new Download(download);
85    // We get downloads in display order, so we don't have to worry about
86    // maintaining correct order - we can assume that any downloads not in
87    // display order are new ones and so we can add them to the top of the
88    // list.
89    if (download.started > this.newestTime_) {
90      this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild);
91      this.newestTime_ = download.started;
92    } else {
93      this.node_.appendChild(this.downloads_[id].node);
94    }
95  }
96  // Download.prototype.update may change its nodeSince_ and nodeDate_, so
97  // update all the date displays.
98  // TODO(benjhayden) Only do this if its nodeSince_ or nodeDate_ actually did
99  // change since this may touch 150 elements and Downloads.prototype.updated
100  // may be called 150 times.
101  this.updateDateDisplay_();
102};
103
104/**
105 * Set our display search text.
106 * @param {string} searchText The string we're searching for.
107 */
108Downloads.prototype.setSearchText = function(searchText) {
109  this.searchText_ = searchText;
110};
111
112/**
113 * Update the summary block above the results
114 */
115Downloads.prototype.updateSummary = function() {
116  if (this.searchText_) {
117    this.summary_.textContent = loadTimeData.getStringF('searchresultsfor',
118                                                        this.searchText_);
119  } else {
120    this.summary_.textContent = loadTimeData.getString('downloads');
121  }
122
123  var hasDownloads = false;
124  for (var i in this.downloads_) {
125    hasDownloads = true;
126    break;
127  }
128};
129
130/**
131 * Returns the number of downloads in the model. Used by tests.
132 * @return {integer} Returns the number of downloads shown on the page.
133 */
134Downloads.prototype.size = function() {
135  return Object.keys(this.downloads_).length;
136};
137
138/**
139 * Update the date visibility in our nodes so that no date is
140 * repeated.
141 * @private
142 */
143Downloads.prototype.updateDateDisplay_ = function() {
144  var dateContainers = document.getElementsByClassName('date-container');
145  var displayed = {};
146  for (var i = 0, container; container = dateContainers[i]; i++) {
147    var dateString = container.getElementsByClassName('date')[0].innerHTML;
148    if (!!displayed[dateString]) {
149      container.style.display = 'none';
150    } else {
151      displayed[dateString] = true;
152      container.style.display = 'block';
153    }
154  }
155};
156
157/**
158 * Remove a download.
159 * @param {number} id The id of the download to remove.
160 */
161Downloads.prototype.remove = function(id) {
162  this.node_.removeChild(this.downloads_[id].node);
163  delete this.downloads_[id];
164  this.updateDateDisplay_();
165};
166
167/**
168 * Clear all downloads and reset us back to a null state.
169 */
170Downloads.prototype.clear = function() {
171  for (var id in this.downloads_) {
172    this.downloads_[id].clear();
173    this.remove(id);
174  }
175};
176
177/**
178 * Schedule icon load.
179 * @param {HTMLImageElement} elem Image element that should contain the icon.
180 * @param {string} iconURL URL to the icon.
181 */
182Downloads.prototype.scheduleIconLoad = function(elem, iconURL) {
183  var self = this;
184
185  // Sends request to the next icon in the queue and schedules
186  // call to itself when the icon is loaded.
187  function loadNext() {
188    self.isIconLoading_ = true;
189    while (self.iconLoadQueue_.length > 0) {
190      var request = self.iconLoadQueue_.shift();
191      var oldSrc = request.element.src;
192      request.element.onabort = request.element.onerror =
193          request.element.onload = loadNext;
194      request.element.src = request.url;
195      if (oldSrc != request.element.src)
196        return;
197    }
198    self.isIconLoading_ = false;
199  }
200
201  // Create new request
202  var loadRequest = {element: elem, url: iconURL};
203  this.iconLoadQueue_.push(loadRequest);
204
205  // Start loading if none scheduled yet
206  if (!this.isIconLoading_)
207    loadNext();
208};
209
210/**
211 * Returns whether the displayed list needs to be updated or not.
212 * @param {Array} downloads Array of download nodes.
213 * @return {boolean} Returns true if the displayed list is to be updated.
214 */
215Downloads.prototype.isUpdateNeeded = function(downloads) {
216  var size = 0;
217  for (var i in this.downloads_)
218    size++;
219  if (size != downloads.length)
220    return true;
221  // Since there are the same number of items in the incoming list as
222  // |this.downloads_|, there won't be any removed downloads without some
223  // downloads having been inserted.  So check only for new downloads in
224  // deciding whether to update.
225  for (var i = 0; i < downloads.length; i++) {
226    if (!this.downloads_[downloads[i].id])
227      return true;
228  }
229  return false;
230};
231
232///////////////////////////////////////////////////////////////////////////////
233// Download
234/**
235 * A download and the DOM representation for that download.
236 * @param {Object} download A backend download object (see downloads_ui.cc)
237 * @constructor
238 */
239function Download(download) {
240  // Create DOM
241  this.node = createElementWithClassName(
242      'div', 'download' + (download.otr ? ' otr' : ''));
243
244  // Dates
245  this.dateContainer_ = createElementWithClassName('div', 'date-container');
246  this.node.appendChild(this.dateContainer_);
247
248  this.nodeSince_ = createElementWithClassName('div', 'since');
249  this.nodeDate_ = createElementWithClassName('div', 'date');
250  this.dateContainer_.appendChild(this.nodeSince_);
251  this.dateContainer_.appendChild(this.nodeDate_);
252
253  // Container for all 'safe download' UI.
254  this.safe_ = createElementWithClassName('div', 'safe');
255  this.safe_.ondragstart = this.drag_.bind(this);
256  this.node.appendChild(this.safe_);
257
258  if (download.state != Download.States.COMPLETE) {
259    this.nodeProgressBackground_ =
260        createElementWithClassName('div', 'progress background');
261    this.safe_.appendChild(this.nodeProgressBackground_);
262
263    this.nodeProgressForeground_ =
264        createElementWithClassName('canvas', 'progress');
265    this.nodeProgressForeground_.width = Download.Progress.width;
266    this.nodeProgressForeground_.height = Download.Progress.height;
267    this.canvasProgress_ = this.nodeProgressForeground_.getContext('2d');
268
269    this.safe_.appendChild(this.nodeProgressForeground_);
270  }
271
272  this.nodeImg_ = createElementWithClassName('img', 'icon');
273  this.safe_.appendChild(this.nodeImg_);
274
275  // FileLink is used for completed downloads, otherwise we show FileName.
276  this.nodeTitleArea_ = createElementWithClassName('div', 'title-area');
277  this.safe_.appendChild(this.nodeTitleArea_);
278
279  this.nodeFileLink_ = createLink(this.openFile_.bind(this), '');
280  this.nodeFileLink_.className = 'name';
281  this.nodeFileLink_.style.display = 'none';
282  this.nodeTitleArea_.appendChild(this.nodeFileLink_);
283
284  this.nodeFileName_ = createElementWithClassName('span', 'name');
285  this.nodeFileName_.style.display = 'none';
286  this.nodeTitleArea_.appendChild(this.nodeFileName_);
287
288  this.nodeStatus_ = createElementWithClassName('span', 'status');
289  this.nodeTitleArea_.appendChild(this.nodeStatus_);
290
291  var nodeURLDiv = createElementWithClassName('div', 'url-container');
292  this.safe_.appendChild(nodeURLDiv);
293
294  this.nodeURL_ = createElementWithClassName('a', 'src-url');
295  this.nodeURL_.target = '_blank';
296  nodeURLDiv.appendChild(this.nodeURL_);
297
298  // Controls.
299  this.nodeControls_ = createElementWithClassName('div', 'controls');
300  this.safe_.appendChild(this.nodeControls_);
301
302  // We don't need 'show in folder' in chromium os. See download_ui.cc and
303  // http://code.google.com/p/chromium-os/issues/detail?id=916.
304  if (loadTimeData.valueExists('control_showinfolder')) {
305    this.controlShow_ = createLink(this.show_.bind(this),
306        loadTimeData.getString('control_showinfolder'));
307    this.nodeControls_.appendChild(this.controlShow_);
308  } else {
309    this.controlShow_ = null;
310  }
311
312  this.controlRetry_ = document.createElement('a');
313  this.controlRetry_.download = '';
314  this.controlRetry_.textContent = loadTimeData.getString('control_retry');
315  this.nodeControls_.appendChild(this.controlRetry_);
316
317  // Pause/Resume are a toggle.
318  this.controlPause_ = createLink(this.pause_.bind(this),
319      loadTimeData.getString('control_pause'));
320  this.nodeControls_.appendChild(this.controlPause_);
321
322  this.controlResume_ = createLink(this.resume_.bind(this),
323      loadTimeData.getString('control_resume'));
324  this.nodeControls_.appendChild(this.controlResume_);
325
326  // Anchors <a> don't support the "disabled" property.
327  if (loadTimeData.getBoolean('allow_deleting_history')) {
328    this.controlRemove_ = createLink(this.remove_.bind(this),
329        loadTimeData.getString('control_removefromlist'));
330    this.controlRemove_.classList.add('control-remove-link');
331  } else {
332    this.controlRemove_ = document.createElement('span');
333    this.controlRemove_.classList.add('disabled-link');
334    var text = document.createTextNode(
335        loadTimeData.getString('control_removefromlist'));
336    this.controlRemove_.appendChild(text);
337  }
338  if (!loadTimeData.getBoolean('show_delete_history'))
339    this.controlRemove_.hidden = true;
340
341  this.nodeControls_.appendChild(this.controlRemove_);
342
343  this.controlCancel_ = createLink(this.cancel_.bind(this),
344      loadTimeData.getString('control_cancel'));
345  this.nodeControls_.appendChild(this.controlCancel_);
346
347  this.controlByExtension_ = document.createElement('span');
348  this.nodeControls_.appendChild(this.controlByExtension_);
349
350  // Container for 'unsafe download' UI.
351  this.danger_ = createElementWithClassName('div', 'show-dangerous');
352  this.node.appendChild(this.danger_);
353
354  this.dangerNodeImg_ = createElementWithClassName('img', 'icon');
355  this.danger_.appendChild(this.dangerNodeImg_);
356
357  this.dangerDesc_ = document.createElement('div');
358  this.danger_.appendChild(this.dangerDesc_);
359
360  // Buttons for the malicious case.
361  this.malwareNodeControls_ = createElementWithClassName('div', 'controls');
362  this.malwareSave_ = createLink(
363      this.saveDangerous_.bind(this),
364      loadTimeData.getString('danger_restore'));
365  this.malwareNodeControls_.appendChild(this.malwareSave_);
366  this.malwareDiscard_ = createLink(
367      this.discardDangerous_.bind(this),
368      loadTimeData.getString('control_removefromlist'));
369  this.malwareNodeControls_.appendChild(this.malwareDiscard_);
370  this.danger_.appendChild(this.malwareNodeControls_);
371
372  // Buttons for the dangerous but not malicious case.
373  this.dangerSave_ = createButton(
374      this.saveDangerous_.bind(this),
375      loadTimeData.getString('danger_save'));
376  this.danger_.appendChild(this.dangerSave_);
377
378  this.dangerDiscard_ = createButton(
379      this.discardDangerous_.bind(this),
380      loadTimeData.getString('danger_discard'));
381  this.danger_.appendChild(this.dangerDiscard_);
382
383  // Update member vars.
384  this.update(download);
385}
386
387/**
388 * The states a download can be in. These correspond to states defined in
389 * DownloadsDOMHandler::CreateDownloadItemValue
390 */
391Download.States = {
392  IN_PROGRESS: 'IN_PROGRESS',
393  CANCELLED: 'CANCELLED',
394  COMPLETE: 'COMPLETE',
395  PAUSED: 'PAUSED',
396  DANGEROUS: 'DANGEROUS',
397  INTERRUPTED: 'INTERRUPTED',
398};
399
400/**
401 * Explains why a download is in DANGEROUS state.
402 */
403Download.DangerType = {
404  NOT_DANGEROUS: 'NOT_DANGEROUS',
405  DANGEROUS_FILE: 'DANGEROUS_FILE',
406  DANGEROUS_URL: 'DANGEROUS_URL',
407  DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
408  UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
409  DANGEROUS_HOST: 'DANGEROUS_HOST',
410  POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED',
411};
412
413/**
414 * @param {number} a Some float.
415 * @param {number} b Some float.
416 * @param {number} opt_pct Percent of min(a,b).
417 * @return {boolean} true if a is within opt_pct percent of b.
418 */
419function floatEq(a, b, opt_pct) {
420  return Math.abs(a - b) < (Math.min(a, b) * (opt_pct || 1.0) / 100.0);
421}
422
423/**
424 * Constants and "constants" for the progress meter.
425 */
426Download.Progress = {
427  START_ANGLE: -0.5 * Math.PI,
428  SIDE: 48,
429};
430
431/***/
432Download.Progress.HALF = Download.Progress.SIDE / 2;
433
434function computeDownloadProgress() {
435  if (floatEq(Download.Progress.scale, window.devicePixelRatio)) {
436    // Zooming in or out multiple times then typing Ctrl+0 resets the zoom level
437    // directly to 1x, which fires the matchMedia event multiple times.
438    return;
439  }
440  Download.Progress.scale = window.devicePixelRatio;
441  Download.Progress.width = Download.Progress.SIDE * Download.Progress.scale;
442  Download.Progress.height = Download.Progress.SIDE * Download.Progress.scale;
443  Download.Progress.radius = Download.Progress.HALF * Download.Progress.scale;
444  Download.Progress.centerX = Download.Progress.HALF * Download.Progress.scale;
445  Download.Progress.centerY = Download.Progress.HALF * Download.Progress.scale;
446}
447computeDownloadProgress();
448
449// Listens for when device-pixel-ratio changes between any zoom level.
450[0.3, 0.4, 0.6, 0.7, 0.8, 0.95, 1.05, 1.2, 1.4, 1.6, 1.9, 2.2, 2.7, 3.5, 4.5
451].forEach(function(scale) {
452  matchMedia('(-webkit-min-device-pixel-ratio:' + scale + ')').addListener(
453    function() {
454      computeDownloadProgress();
455  });
456});
457
458var ImageCache = {};
459function getCachedImage(src) {
460  if (!ImageCache[src]) {
461    ImageCache[src] = new Image();
462    ImageCache[src].src = src;
463  }
464  return ImageCache[src];
465}
466
467/**
468 * Updates the download to reflect new data.
469 * @param {Object} download A backend download object (see downloads_ui.cc)
470 */
471Download.prototype.update = function(download) {
472  this.id_ = download.id;
473  this.filePath_ = download.file_path;
474  this.fileUrl_ = download.file_url;
475  this.fileName_ = download.file_name;
476  this.url_ = download.url;
477  this.state_ = download.state;
478  this.fileExternallyRemoved_ = download.file_externally_removed;
479  this.dangerType_ = download.danger_type;
480  this.lastReasonDescription_ = download.last_reason_text;
481  this.byExtensionId_ = download.by_ext_id;
482  this.byExtensionName_ = download.by_ext_name;
483
484  this.since_ = download.since_string;
485  this.date_ = download.date_string;
486
487  // See DownloadItem::PercentComplete
488  this.percent_ = Math.max(download.percent, 0);
489  this.progressStatusText_ = download.progress_status_text;
490  this.received_ = download.received;
491
492  if (this.state_ == Download.States.DANGEROUS) {
493    this.updateDangerousFile();
494  } else {
495    downloads.scheduleIconLoad(this.nodeImg_,
496                               'chrome://fileicon/' +
497                                   encodeURIComponent(this.filePath_) +
498                                   '?scale=' + window.devicePixelRatio + 'x');
499
500    if (this.state_ == Download.States.COMPLETE &&
501        !this.fileExternallyRemoved_) {
502      this.nodeFileLink_.textContent = this.fileName_;
503      this.nodeFileLink_.href = this.fileUrl_;
504      this.nodeFileLink_.oncontextmenu = null;
505    } else if (this.nodeFileName_.textContent != this.fileName_) {
506      this.nodeFileName_.textContent = this.fileName_;
507    }
508    if (this.state_ == Download.States.INTERRUPTED) {
509      this.nodeFileName_.classList.add('interrupted');
510    } else if (this.nodeFileName_.classList.contains('interrupted')) {
511      this.nodeFileName_.classList.remove('interrupted');
512    }
513
514    showInline(this.nodeFileLink_,
515               this.state_ == Download.States.COMPLETE &&
516                   !this.fileExternallyRemoved_);
517    // nodeFileName_ has to be inline-block to avoid the 'interaction' with
518    // nodeStatus_. If both are inline, it appears that their text contents
519    // are merged before the bidi algorithm is applied leading to an
520    // undesirable reordering. http://crbug.com/13216
521    showInlineBlock(this.nodeFileName_,
522                    this.state_ != Download.States.COMPLETE ||
523                        this.fileExternallyRemoved_);
524
525    if (this.state_ == Download.States.IN_PROGRESS) {
526      this.nodeProgressForeground_.style.display = 'block';
527      this.nodeProgressBackground_.style.display = 'block';
528      this.nodeProgressForeground_.width = Download.Progress.width;
529      this.nodeProgressForeground_.height = Download.Progress.height;
530
531      var foregroundImage = getCachedImage(
532          'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32@' +
533          window.devicePixelRatio + 'x');
534
535      // Draw a pie-slice for the progress.
536      this.canvasProgress_.globalCompositeOperation = 'copy';
537      this.canvasProgress_.drawImage(
538          foregroundImage,
539          0, 0,  // sx, sy
540          foregroundImage.width,
541          foregroundImage.height,
542          0, 0,  // x, y
543          Download.Progress.width, Download.Progress.height);
544      this.canvasProgress_.globalCompositeOperation = 'destination-in';
545      this.canvasProgress_.beginPath();
546      this.canvasProgress_.moveTo(Download.Progress.centerX,
547                                  Download.Progress.centerY);
548
549      // Draw an arc CW for both RTL and LTR. http://crbug.com/13215
550      this.canvasProgress_.arc(Download.Progress.centerX,
551                               Download.Progress.centerY,
552                               Download.Progress.radius,
553                               Download.Progress.START_ANGLE,
554                               Download.Progress.START_ANGLE + Math.PI * 0.02 *
555                               Number(this.percent_),
556                               false);
557
558      this.canvasProgress_.lineTo(Download.Progress.centerX,
559                                  Download.Progress.centerY);
560      this.canvasProgress_.fill();
561      this.canvasProgress_.closePath();
562    } else if (this.nodeProgressBackground_) {
563      this.nodeProgressForeground_.style.display = 'none';
564      this.nodeProgressBackground_.style.display = 'none';
565    }
566
567    if (this.controlShow_) {
568      showInline(this.controlShow_,
569                 this.state_ == Download.States.COMPLETE &&
570                     !this.fileExternallyRemoved_);
571    }
572    showInline(this.controlRetry_, download.retry);
573    this.controlRetry_.href = this.url_;
574    showInline(this.controlPause_, this.state_ == Download.States.IN_PROGRESS);
575    showInline(this.controlResume_, download.resume);
576    var showCancel = this.state_ == Download.States.IN_PROGRESS ||
577                     this.state_ == Download.States.PAUSED;
578    showInline(this.controlCancel_, showCancel);
579    showInline(this.controlRemove_, !showCancel);
580
581    if (this.byExtensionId_ && this.byExtensionName_) {
582      // Format 'control_by_extension' with a link instead of plain text by
583      // splitting the formatted string into pieces.
584      var slug = 'XXXXX';
585      var formatted = loadTimeData.getStringF('control_by_extension', slug);
586      var slugIndex = formatted.indexOf(slug);
587      this.controlByExtension_.textContent = formatted.substr(0, slugIndex);
588      this.controlByExtensionLink_ = document.createElement('a');
589      this.controlByExtensionLink_.href =
590          'chrome://extensions#' + this.byExtensionId_;
591      this.controlByExtensionLink_.textContent = this.byExtensionName_;
592      this.controlByExtension_.appendChild(this.controlByExtensionLink_);
593      if (slugIndex < (formatted.length - slug.length))
594        this.controlByExtension_.appendChild(document.createTextNode(
595            formatted.substr(slugIndex + 1)));
596    }
597
598    this.nodeSince_.textContent = this.since_;
599    this.nodeDate_.textContent = this.date_;
600    // Don't unnecessarily update the url, as doing so will remove any
601    // text selection the user has started (http://crbug.com/44982).
602    if (this.nodeURL_.textContent != this.url_) {
603      this.nodeURL_.textContent = this.url_;
604      this.nodeURL_.href = this.url_;
605    }
606    this.nodeStatus_.textContent = this.getStatusText_();
607
608    this.danger_.style.display = 'none';
609    this.safe_.style.display = 'block';
610  }
611};
612
613/**
614 * Decorates the icons, strings, and buttons for a download to reflect the
615 * danger level of a file. Dangerous & malicious files are treated differently.
616 */
617Download.prototype.updateDangerousFile = function() {
618  switch (this.dangerType_) {
619    case Download.DangerType.DANGEROUS_FILE: {
620      this.dangerDesc_.textContent = loadTimeData.getStringF(
621          'danger_file_desc', this.fileName_);
622      break;
623    }
624    case Download.DangerType.DANGEROUS_URL: {
625      this.dangerDesc_.textContent = loadTimeData.getString('danger_url_desc');
626      break;
627    }
628    case Download.DangerType.DANGEROUS_CONTENT:  // Fall through.
629    case Download.DangerType.DANGEROUS_HOST: {
630      this.dangerDesc_.textContent = loadTimeData.getStringF(
631          'danger_content_desc', this.fileName_);
632      break;
633    }
634    case Download.DangerType.UNCOMMON_CONTENT: {
635      this.dangerDesc_.textContent = loadTimeData.getStringF(
636          'danger_uncommon_desc', this.fileName_);
637      break;
638    }
639    case Download.DangerType.POTENTIALLY_UNWANTED: {
640      this.dangerDesc_.textContent = loadTimeData.getStringF(
641          'danger_settings_desc', this.fileName_);
642      break;
643    }
644  }
645
646  if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) {
647    downloads.scheduleIconLoad(
648        this.dangerNodeImg_,
649        'chrome://theme/IDR_WARNING?scale=' + window.devicePixelRatio + 'x');
650  } else {
651    downloads.scheduleIconLoad(
652        this.dangerNodeImg_,
653        'chrome://theme/IDR_SAFEBROWSING_WARNING?scale=' +
654            window.devicePixelRatio + 'x');
655    this.dangerDesc_.className = 'malware-description';
656  }
657
658  if (this.dangerType_ == Download.DangerType.DANGEROUS_CONTENT ||
659      this.dangerType_ == Download.DangerType.DANGEROUS_HOST ||
660      this.dangerType_ == Download.DangerType.DANGEROUS_URL ||
661      this.dangerType_ == Download.DangerType.POTENTIALLY_UNWANTED) {
662    this.malwareNodeControls_.style.display = 'block';
663    this.dangerDiscard_.style.display = 'none';
664    this.dangerSave_.style.display = 'none';
665  } else {
666    this.malwareNodeControls_.style.display = 'none';
667    this.dangerDiscard_.style.display = 'inline';
668    this.dangerSave_.style.display = 'inline';
669  }
670
671  this.danger_.style.display = 'block';
672  this.safe_.style.display = 'none';
673};
674
675/**
676 * Removes applicable bits from the DOM in preparation for deletion.
677 */
678Download.prototype.clear = function() {
679  this.safe_.ondragstart = null;
680  this.nodeFileLink_.onclick = null;
681  if (this.controlShow_) {
682    this.controlShow_.onclick = null;
683  }
684  this.controlCancel_.onclick = null;
685  this.controlPause_.onclick = null;
686  this.controlResume_.onclick = null;
687  this.dangerDiscard_.onclick = null;
688  this.dangerSave_.onclick = null;
689  this.malwareDiscard_.onclick = null;
690  this.malwareSave_.onclick = null;
691
692  this.node.innerHTML = '';
693};
694
695/**
696 * @private
697 * @return {string} User-visible status update text.
698 */
699Download.prototype.getStatusText_ = function() {
700  switch (this.state_) {
701    case Download.States.IN_PROGRESS:
702      return this.progressStatusText_;
703    case Download.States.CANCELLED:
704      return loadTimeData.getString('status_cancelled');
705    case Download.States.PAUSED:
706      return loadTimeData.getString('status_paused');
707    case Download.States.DANGEROUS:
708      // danger_url_desc is also used by DANGEROUS_CONTENT.
709      var desc = this.dangerType_ == Download.DangerType.DANGEROUS_FILE ?
710          'danger_file_desc' : 'danger_url_desc';
711      return loadTimeData.getString(desc);
712    case Download.States.INTERRUPTED:
713      return this.lastReasonDescription_;
714    case Download.States.COMPLETE:
715      return this.fileExternallyRemoved_ ?
716          loadTimeData.getString('status_removed') : '';
717  }
718};
719
720/**
721 * Tells the backend to initiate a drag, allowing users to drag
722 * files from the download page and have them appear as native file
723 * drags.
724 * @return {boolean} Returns false to prevent the default action.
725 * @private
726 */
727Download.prototype.drag_ = function() {
728  chrome.send('drag', [this.id_.toString()]);
729  return false;
730};
731
732/**
733 * Tells the backend to open this file.
734 * @return {boolean} Returns false to prevent the default action.
735 * @private
736 */
737Download.prototype.openFile_ = function() {
738  chrome.send('openFile', [this.id_.toString()]);
739  return false;
740};
741
742/**
743 * Tells the backend that the user chose to save a dangerous file.
744 * @return {boolean} Returns false to prevent the default action.
745 * @private
746 */
747Download.prototype.saveDangerous_ = function() {
748  chrome.send('saveDangerous', [this.id_.toString()]);
749  return false;
750};
751
752/**
753 * Tells the backend that the user chose to discard a dangerous file.
754 * @return {boolean} Returns false to prevent the default action.
755 * @private
756 */
757Download.prototype.discardDangerous_ = function() {
758  chrome.send('discardDangerous', [this.id_.toString()]);
759  downloads.remove(this.id_);
760  return false;
761};
762
763/**
764 * Tells the backend to show the file in explorer.
765 * @return {boolean} Returns false to prevent the default action.
766 * @private
767 */
768Download.prototype.show_ = function() {
769  chrome.send('show', [this.id_.toString()]);
770  return false;
771};
772
773/**
774 * Tells the backend to pause this download.
775 * @return {boolean} Returns false to prevent the default action.
776 * @private
777 */
778Download.prototype.pause_ = function() {
779  chrome.send('pause', [this.id_.toString()]);
780  return false;
781};
782
783/**
784 * Tells the backend to resume this download.
785 * @return {boolean} Returns false to prevent the default action.
786 * @private
787 */
788Download.prototype.resume_ = function() {
789  chrome.send('resume', [this.id_.toString()]);
790  return false;
791};
792
793/**
794 * Tells the backend to remove this download from history and download shelf.
795 * @return {boolean} Returns false to prevent the default action.
796 * @private
797 */
798 Download.prototype.remove_ = function() {
799   if (loadTimeData.getBoolean('allow_deleting_history')) {
800    chrome.send('remove', [this.id_.toString()]);
801  }
802  return false;
803};
804
805/**
806 * Tells the backend to cancel this download.
807 * @return {boolean} Returns false to prevent the default action.
808 * @private
809 */
810Download.prototype.cancel_ = function() {
811  chrome.send('cancel', [this.id_.toString()]);
812  return false;
813};
814
815///////////////////////////////////////////////////////////////////////////////
816// Page:
817var downloads, resultsTimeout;
818
819// TODO(benjhayden): Rename Downloads to DownloadManager, downloads to
820// downloadManager or theDownloadManager or DownloadManager.get() to prevent
821// confusing Downloads with Download.
822
823/**
824 * The FIFO array that stores updates of download files to be appeared
825 * on the download page. It is guaranteed that the updates in this array
826 * are reflected to the download page in a FIFO order.
827*/
828var fifoResults;
829
830function load() {
831  chrome.send('onPageLoaded');
832  fifoResults = [];
833  downloads = new Downloads();
834  $('term').focus();
835  setSearch('');
836
837  var clearAllHolder = $('clear-all-holder');
838  var clearAllElement;
839  if (loadTimeData.getBoolean('allow_deleting_history')) {
840    clearAllElement = createLink(clearAll, loadTimeData.getString('clear_all'));
841    clearAllElement.classList.add('clear-all-link');
842    clearAllHolder.classList.remove('disabled-link');
843  } else {
844    clearAllElement = document.createTextNode(
845        loadTimeData.getString('clear_all'));
846    clearAllHolder.classList.add('disabled-link');
847  }
848  if (!loadTimeData.getBoolean('show_delete_history'))
849    clearAllHolder.hidden = true;
850
851  clearAllHolder.appendChild(clearAllElement);
852  clearAllElement.oncontextmenu = function() { return false; };
853
854  // TODO(jhawkins): Use a link-button here.
855  var openDownloadsFolderLink = $('open-downloads-folder');
856  openDownloadsFolderLink.onclick = function() {
857    chrome.send('openDownloadsFolder');
858  };
859  openDownloadsFolderLink.oncontextmenu = function() { return false; };
860
861  $('search-link').onclick = function(e) {
862    setSearch('');
863    e.preventDefault();
864    $('term').value = '';
865    return false;
866  };
867
868  $('term').onsearch = function(e) {
869    setSearch(this.value);
870  };
871}
872
873function setSearch(searchText) {
874  fifoResults.length = 0;
875  downloads.setSearchText(searchText);
876  searchText = searchText.toString().match(/(?:[^\s"]+|"[^"]*")+/g);
877  if (searchText) {
878    searchText = searchText.map(function(term) {
879      // strip quotes
880      return (term.match(/\s/) &&
881              term[0].match(/["']/) &&
882              term[term.length - 1] == term[0]) ?
883        term.substr(1, term.length - 2) : term;
884    });
885  } else {
886    searchText = [];
887  }
888  chrome.send('getDownloads', searchText);
889}
890
891function clearAll() {
892  if (!loadTimeData.getBoolean('allow_deleting_history'))
893    return;
894
895  fifoResults.length = 0;
896  downloads.clear();
897  downloads.setSearchText('');
898  chrome.send('clearAll');
899}
900
901///////////////////////////////////////////////////////////////////////////////
902// Chrome callbacks:
903/**
904 * Our history system calls this function with results from searches or when
905 * downloads are added or removed.
906 * @param {Array.<Object>} results List of updates.
907 */
908function downloadsList(results) {
909  if (downloads && downloads.isUpdateNeeded(results)) {
910    if (resultsTimeout)
911      clearTimeout(resultsTimeout);
912    fifoResults.length = 0;
913    downloads.clear();
914    downloadUpdated(results);
915  }
916  downloads.updateSummary();
917}
918
919/**
920 * When a download is updated (progress, state change), this is called.
921 * @param {Array.<Object>} results List of updates for the download process.
922 */
923function downloadUpdated(results) {
924  // Sometimes this can get called too early.
925  if (!downloads)
926    return;
927
928  fifoResults = fifoResults.concat(results);
929  tryDownloadUpdatedPeriodically();
930}
931
932/**
933 * Try to reflect as much updates as possible within 50ms.
934 * This function is scheduled again and again until all updates are reflected.
935 */
936function tryDownloadUpdatedPeriodically() {
937  var start = Date.now();
938  while (fifoResults.length) {
939    var result = fifoResults.shift();
940    downloads.updated(result);
941    // Do as much as we can in 50ms.
942    if (Date.now() - start > 50) {
943      clearTimeout(resultsTimeout);
944      resultsTimeout = setTimeout(tryDownloadUpdatedPeriodically, 5);
945      break;
946    }
947  }
948}
949
950// Add handlers to HTML elements.
951window.addEventListener('DOMContentLoaded', load);
952