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