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