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