1 <!DOCTYPE HTML> 2 <html i18n-values="dir:textdirection;"> 3 <head> 4 <meta charset="utf-8"> 5 <title i18n-content="title"></title> 6 <link rel="icon" href="../../app/theme/downloads_favicon.png"> 7 <style> 8 body { 9 background-color: white; 10 color: black; 11 margin: 10px; 12 } 13 14 .header { 15 overflow: auto; 16 clear: both; 17 } 18 19 .header .logo { 20 float: left; 21 } 22 23 .header .form { 24 float: left; 25 margin-top: 22px; 26 -webkit-margin-start: 12px; 27 } 28 29 html[dir=rtl] .logo, html[dir=rtl] .form { 30 float: right; 31 } 32 33 #downloads-summary { 34 margin-top: 12px; 35 border-top: 1px solid #9cc2ef; 36 background-color: #ebeff9; 37 padding: 3px; 38 margin-bottom: 6px; 39 } 40 41 #downloads-summary-text { 42 font-weight: bold; 43 } 44 45 #downloads-summary > a { 46 float: right; 47 } 48 49 html[dir=rtl] #downloads-summary > a { 50 float: left; 51 } 52 53 #downloads-display { 54 max-width: 740px; 55 } 56 57 .download { 58 position: relative; 59 margin-top: 6px; 60 -webkit-margin-start: 114px; 61 -webkit-padding-start: 56px; 62 margin-bottom: 15px; 63 } 64 65 .date-container { 66 position: absolute; 67 left: -110px; 68 width: 110px; 69 } 70 71 html[dir=rtl] .date-container { 72 left: auto; 73 right: -110px; 74 } 75 76 .date-container .since { 77 color: black; 78 } 79 80 .date-container .date { 81 color: #666; 82 } 83 84 .download .icon { 85 position: absolute; 86 top: 2px; 87 left: 9px; 88 width: 32px; 89 height: 32px; 90 } 91 92 html[dir=rtl] .icon { 93 left: auto; 94 right: 9px; 95 } 96 97 .download.otr > .safe, 98 .download.otr > .show-dangerous { 99 background: url('shared/images/otr_icon_standalone.png') no-repeat 100% 100%; 100 opacity: .66; 101 -webkit-transition: opacity .15s; 102 } 103 104 html[dir=rtl] .download.otr > .safe, 105 html[dir=rtl] .download.otr > .show-dangerous { 106 background-position: 0% 100%; 107 } 108 109 .download.otr > .safe:hover, 110 .download.otr > .show-dangerous:hover { 111 opacity: 1; 112 } 113 114 .progress { 115 position: absolute; 116 top: -6px; 117 left: 0px; 118 width: 48px; 119 height: 48px; 120 } 121 122 html[dir=rtl] .progress { 123 left: auto; 124 right: 0px; 125 } 126 127 .progress.background { 128 background: url('../../app/theme/download_progress_background32.png'); 129 } 130 131 .progress.foreground { 132 background: url('../../app/theme/download_progress_foreground32.png'); 133 } 134 135 .name { 136 display: none; 137 -webkit-padding-end: 16px; 138 max-width: 450px; 139 word-break: break-all; 140 } 141 142 .download .status { 143 display: inline; 144 color: #999; 145 white-space: nowrap; 146 } 147 148 .download .url { 149 color: #080; 150 max-width: 500px; 151 white-space: nowrap; 152 overflow: hidden; 153 text-overflow: ellipsis; 154 } 155 156 .controls a { 157 color: #777; 158 margin-right: 16px; 159 } 160 161 #downloads-pagination { 162 padding-top: 24px; 163 margin-left: 18px; 164 } 165 166 .page-navigation { 167 padding: 8px; 168 background-color: #ebeff9; 169 margin-right: 4px; 170 } 171 172 .footer { 173 height: 24px; 174 } 175 176 </style> 177 <script src="shared/js/local_strings.js"></script> 178 <script> 179 180 /////////////////////////////////////////////////////////////////////////////// 181 // Helper functions 182 function $(o) {return document.getElementById(o);} 183 184 /** 185 * Sets the display style of a node. 186 */ 187 function showInline(node, isShow) { 188 node.style.display = isShow ? 'inline' : 'none'; 189 } 190 191 function showInlineBlock(node, isShow) { 192 node.style.display = isShow ? 'inline-block' : 'none'; 193 } 194 195 /** 196 * Creates an element of a specified type with a specified class name. 197 * @param {String} type The node type. 198 * @param {String} className The class name to use. 199 */ 200 function createElementWithClassName(type, className) { 201 var elm = document.createElement(type); 202 elm.className = className; 203 return elm; 204 } 205 206 /** 207 * Creates a link with a specified onclick handler and content 208 * @param {String} onclick The onclick handler 209 * @param {String} value The link text 210 */ 211 function createLink(onclick, value) { 212 var link = document.createElement('a'); 213 link.onclick = onclick; 214 link.href = '#'; 215 link.innerHTML = value; 216 return link; 217 } 218 219 /** 220 * Creates a button with a specified onclick handler and content 221 * @param {String} onclick The onclick handler 222 * @param {String} value The button text 223 */ 224 function createButton(onclick, value) { 225 var button = document.createElement('input'); 226 button.type = 'button'; 227 button.value = value; 228 button.onclick = onclick; 229 return button; 230 } 231 232 /////////////////////////////////////////////////////////////////////////////// 233 // Downloads 234 /** 235 * Class to hold all the information about the visible downloads. 236 */ 237 function Downloads() { 238 this.downloads_ = {}; 239 this.node_ = $('downloads-display'); 240 this.summary_ = $('downloads-summary-text'); 241 this.searchText_ = ''; 242 243 // Keep track of the dates of the newest and oldest downloads so that we 244 // know where to insert them. 245 this.newestTime_ = -1; 246 } 247 248 /** 249 * Called when a download has been updated or added. 250 * @param {Object} download A backend download object (see downloads_ui.cc) 251 */ 252 Downloads.prototype.updated = function(download) { 253 var id = download.id; 254 if (!!this.downloads_[id]) { 255 this.downloads_[id].update(download); 256 } else { 257 this.downloads_[id] = new Download(download); 258 // We get downloads in display order, so we don't have to worry about 259 // maintaining correct order - we can assume that any downloads not in 260 // display order are new ones and so we can add them to the top of the 261 // list. 262 if (download.started > this.newestTime_) { 263 this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild); 264 this.newestTime_ = download.started; 265 } else { 266 this.node_.appendChild(this.downloads_[id].node); 267 } 268 this.updateDateDisplay_(); 269 } 270 } 271 272 /** 273 * Set our display search text. 274 * @param {String} searchText The string we're searching for. 275 */ 276 Downloads.prototype.setSearchText = function(searchText) { 277 this.searchText_ = searchText; 278 } 279 280 /** 281 * Update the summary block above the results 282 */ 283 Downloads.prototype.updateSummary = function() { 284 if (this.searchText_) { 285 this.summary_.textContent = localStrings.getStringF('searchresultsfor', 286 this.searchText_); 287 } else { 288 this.summary_.innerHTML = localStrings.getString('downloads'); 289 } 290 291 var hasDownloads = false; 292 for (var i in this.downloads_) { 293 hasDownloads = true; 294 break; 295 } 296 297 if (!hasDownloads) { 298 this.node_.innerHTML = localStrings.getString('noresults'); 299 } 300 } 301 302 /** 303 * Update the date visibility in our nodes so that no date is 304 * repeated. 305 */ 306 Downloads.prototype.updateDateDisplay_ = function() { 307 var dateContainers = document.getElementsByClassName('date-container'); 308 var displayed = {}; 309 for (var i = 0, container; container = dateContainers[i]; i++) { 310 var dateString = container.getElementsByClassName('date')[0].innerHTML; 311 if (!!displayed[dateString]) { 312 container.style.display = 'none'; 313 } else { 314 displayed[dateString] = true; 315 container.style.display = 'block'; 316 } 317 } 318 } 319 320 /** 321 * Remove a download. 322 * @param {Number} id The id of the download to remove. 323 */ 324 Downloads.prototype.remove = function(id) { 325 this.node_.removeChild(this.downloads_[id].node); 326 delete this.downloads_[id]; 327 this.updateDateDisplay_(); 328 } 329 330 /** 331 * Clear all downloads and reset us back to a null state. 332 */ 333 Downloads.prototype.clear = function() { 334 for (var id in this.downloads_) { 335 this.downloads_[id].clear(); 336 this.remove(id); 337 } 338 } 339 340 /////////////////////////////////////////////////////////////////////////////// 341 // Download 342 /** 343 * A download and the DOM representation for that download. 344 * @param {Object} download A backend download object (see downloads_ui.cc) 345 */ 346 function Download(download) { 347 // Create DOM 348 this.node = createElementWithClassName('div','download' + 349 (download.otr ? ' otr' : '')); 350 351 // Dates 352 this.dateContainer_ = createElementWithClassName('div', 'date-container'); 353 this.node.appendChild(this.dateContainer_); 354 355 this.nodeSince_ = createElementWithClassName('div', 'since'); 356 this.nodeDate_ = createElementWithClassName('div', 'date'); 357 this.dateContainer_.appendChild(this.nodeSince_); 358 this.dateContainer_.appendChild(this.nodeDate_); 359 360 // Container for all 'safe download' UI. 361 this.safe_ = createElementWithClassName('div', 'safe'); 362 this.safe_.ondragstart = this.drag_.bind(this); 363 this.node.appendChild(this.safe_); 364 365 if (download.state != Download.States.COMPLETE) { 366 this.nodeProgressBackground_ = 367 createElementWithClassName('div', 'progress background'); 368 this.safe_.appendChild(this.nodeProgressBackground_); 369 370 this.canvasProgress_ = 371 document.getCSSCanvasContext('2d', 'canvas_' + download.id, 372 Download.Progress.width, 373 Download.Progress.height); 374 375 this.nodeProgressForeground_ = 376 createElementWithClassName('div', 'progress foreground'); 377 this.nodeProgressForeground_.style.webkitMask = 378 '-webkit-canvas(canvas_'+download.id+')'; 379 this.safe_.appendChild(this.nodeProgressForeground_); 380 } 381 382 this.nodeImg_ = createElementWithClassName('img', 'icon'); 383 this.safe_.appendChild(this.nodeImg_); 384 385 // FileLink is used for completed downloads, otherwise we show FileName. 386 this.nodeTitleArea_ = createElementWithClassName('div', 'title-area'); 387 this.safe_.appendChild(this.nodeTitleArea_); 388 389 this.nodeFileLink_ = createLink(this.openFile_.bind(this), ''); 390 this.nodeFileLink_.className = 'name'; 391 this.nodeFileLink_.style.display = 'none'; 392 this.nodeTitleArea_.appendChild(this.nodeFileLink_); 393 394 this.nodeFileName_ = createElementWithClassName('span', 'name'); 395 this.nodeFileName_.style.display = 'none'; 396 this.nodeTitleArea_.appendChild(this.nodeFileName_); 397 398 this.nodeStatus_ = createElementWithClassName('span', 'status'); 399 this.nodeTitleArea_.appendChild(this.nodeStatus_); 400 401 this.nodeURL_ = createElementWithClassName('div', 'url'); 402 this.safe_.appendChild(this.nodeURL_); 403 404 // Controls. 405 this.nodeControls_ = createElementWithClassName('div', 'controls'); 406 this.safe_.appendChild(this.nodeControls_); 407 408 // We don't need "show in folder" in chromium os. See download_ui.cc and 409 // http://code.google.com/p/chromium-os/issues/detail?id=916. 410 var showinfolder = localStrings.getString('control_showinfolder'); 411 if (showinfolder) { 412 this.controlShow_ = createLink(this.show_.bind(this), showinfolder); 413 this.nodeControls_.appendChild(this.controlShow_); 414 } else { 415 this.controlShow_ = null; 416 } 417 418 this.controlRetry_ = document.createElement('a'); 419 this.controlRetry_.textContent = localStrings.getString('control_retry'); 420 this.nodeControls_.appendChild(this.controlRetry_); 421 422 // Pause/Resume are a toggle. 423 this.controlPause_ = createLink(this.togglePause_.bind(this), 424 localStrings.getString('control_pause')); 425 this.nodeControls_.appendChild(this.controlPause_); 426 427 this.controlResume_ = createLink(this.togglePause_.bind(this), 428 localStrings.getString('control_resume')); 429 this.nodeControls_.appendChild(this.controlResume_); 430 431 this.controlRemove_ = createLink(this.remove_.bind(this), 432 localStrings.getString('control_removefromlist')); 433 this.nodeControls_.appendChild(this.controlRemove_); 434 435 this.controlCancel_ = createLink(this.cancel_.bind(this), 436 localStrings.getString('control_cancel')); 437 this.nodeControls_.appendChild(this.controlCancel_); 438 439 // Container for 'unsafe download' UI. 440 this.danger_ = createElementWithClassName('div', 'show-dangerous'); 441 this.node.appendChild(this.danger_); 442 443 this.dangerDesc_ = document.createElement('div'); 444 this.danger_.appendChild(this.dangerDesc_); 445 446 this.dangerSave_ = createButton(this.saveDangerous_.bind(this), 447 localStrings.getString('danger_save')); 448 this.danger_.appendChild(this.dangerSave_); 449 450 this.dangerDiscard_ = createButton(this.discardDangerous_.bind(this), 451 localStrings.getString('danger_discard')); 452 this.danger_.appendChild(this.dangerDiscard_); 453 454 // Update member vars. 455 this.update(download); 456 } 457 458 /** 459 * The states a download can be in. These correspond to states defined in 460 * DownloadsDOMHandler::CreateDownloadItemValue 461 */ 462 Download.States = { 463 IN_PROGRESS : "IN_PROGRESS", 464 CANCELLED : "CANCELLED", 465 COMPLETE : "COMPLETE", 466 PAUSED : "PAUSED", 467 DANGEROUS : "DANGEROUS", 468 INTERRUPTED : "INTERRUPTED", 469 } 470 471 /** 472 * Explains why a download is in DANGEROUS state. 473 */ 474 Download.DangerType = { 475 NOT_DANGEROUS: "NOT_DANGEROUS", 476 DANGEROUS_FILE: "DANGEROUS_FILE", 477 DANGEROUS_URL: "DANGEROUS_URL", 478 } 479 480 /** 481 * Constants for the progress meter. 482 */ 483 Download.Progress = { 484 width : 48, 485 height : 48, 486 radius : 24, 487 centerX : 24, 488 centerY : 24, 489 base : -0.5 * Math.PI, 490 dir : false, 491 } 492 493 /** 494 * Updates the download to reflect new data. 495 * @param {Object} download A backend download object (see downloads_ui.cc) 496 */ 497 Download.prototype.update = function(download) { 498 this.id_ = download.id; 499 this.filePath_ = download.file_path; 500 this.fileName_ = download.file_name; 501 this.url_ = download.url; 502 this.state_ = download.state; 503 this.dangerType_ = download.danger_type; 504 505 this.since_ = download.since_string; 506 this.date_ = download.date_string; 507 508 // See DownloadItem::PercentComplete 509 this.percent_ = Math.max(download.percent, 0); 510 this.progressStatusText_ = download.progress_status_text; 511 this.received_ = download.received; 512 513 if (this.state_ == Download.States.DANGEROUS) { 514 if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) { 515 this.dangerDesc_.innerHTML = localStrings.getStringF('danger_file_desc', 516 this.fileName_); 517 } else { 518 this.dangerDesc_.innerHTML = localStrings.getString('danger_url_desc'); 519 } 520 this.danger_.style.display = 'block'; 521 this.safe_.style.display = 'none'; 522 } else { 523 this.nodeImg_.src = 'chrome://fileicon/' + this.filePath_; 524 525 if (this.state_ == Download.States.COMPLETE) { 526 this.nodeFileLink_.innerHTML = this.fileName_; 527 this.nodeFileLink_.href = this.filePath_; 528 } else { 529 this.nodeFileName_.innerHTML = this.fileName_; 530 } 531 532 showInline(this.nodeFileLink_, this.state_ == Download.States.COMPLETE); 533 // nodeFileName_ has to be inline-block to avoid the 'interaction' with 534 // nodeStatus_. If both are inline, it appears that their text contents 535 // are merged before the bidi algorithm is applied leading to an 536 // undesirable reordering. http://crbug.com/13216 537 showInlineBlock(this.nodeFileName_, this.state_ != Download.States.COMPLETE); 538 539 if (this.state_ == Download.States.IN_PROGRESS) { 540 this.nodeProgressForeground_.style.display = 'block'; 541 this.nodeProgressBackground_.style.display = 'block'; 542 543 // Draw a pie-slice for the progress. 544 this.canvasProgress_.clearRect(0, 0, 545 Download.Progress.width, 546 Download.Progress.height); 547 this.canvasProgress_.beginPath(); 548 this.canvasProgress_.moveTo(Download.Progress.centerX, 549 Download.Progress.centerY); 550 551 // Draw an arc CW for both RTL and LTR. http://crbug.com/13215 552 this.canvasProgress_.arc(Download.Progress.centerX, 553 Download.Progress.centerY, 554 Download.Progress.radius, 555 Download.Progress.base, 556 Download.Progress.base + Math.PI * 0.02 * 557 Number(this.percent_), 558 false); 559 560 this.canvasProgress_.lineTo(Download.Progress.centerX, 561 Download.Progress.centerY); 562 this.canvasProgress_.fill(); 563 this.canvasProgress_.closePath(); 564 } else if (this.nodeProgressBackground_) { 565 this.nodeProgressForeground_.style.display = 'none'; 566 this.nodeProgressBackground_.style.display = 'none'; 567 } 568 569 if (this.controlShow_) { 570 showInline(this.controlShow_, this.state_ == Download.States.COMPLETE); 571 } 572 showInline(this.controlRetry_, this.state_ == Download.States.CANCELLED); 573 this.controlRetry_.href = this.url_; 574 showInline(this.controlPause_, this.state_ == Download.States.IN_PROGRESS); 575 showInline(this.controlResume_, this.state_ == Download.States.PAUSED); 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 this.nodeSince_.innerHTML = this.since_; 582 this.nodeDate_.innerHTML = this.date_; 583 // Don't unnecessarily update the url, as doing so will remove any 584 // text selection the user has started (http://crbug.com/44982). 585 if (this.nodeURL_.textContent != this.url_) 586 this.nodeURL_.textContent = this.url_; 587 this.nodeStatus_.innerHTML = this.getStatusText_(); 588 589 this.danger_.style.display = 'none'; 590 this.safe_.style.display = 'block'; 591 } 592 } 593 594 /** 595 * Removes applicable bits from the DOM in preparation for deletion. 596 */ 597 Download.prototype.clear = function() { 598 this.safe_.ondragstart = null; 599 this.nodeFileLink_.onclick = null; 600 if (this.controlShow_) { 601 this.controlShow_.onclick = null; 602 } 603 this.controlCancel_.onclick = null; 604 this.controlPause_.onclick = null; 605 this.controlResume_.onclick = null; 606 this.dangerDiscard_.onclick = null; 607 608 this.node.innerHTML = ''; 609 } 610 611 /** 612 * @return {String} User-visible status update text. 613 */ 614 Download.prototype.getStatusText_ = function() { 615 switch (this.state_) { 616 case Download.States.IN_PROGRESS: 617 return this.progressStatusText_; 618 case Download.States.CANCELLED: 619 return localStrings.getString('status_cancelled'); 620 case Download.States.PAUSED: 621 return localStrings.getString('status_paused'); 622 case Download.States.DANGEROUS: 623 var desc = this.dangerType_ == Download.DangerType.DANGEROUS_FILE ? 624 'danger_file_desc' : 'danger_url_desc'; 625 return localStrings.getString(desc); 626 case Download.States.INTERRUPTED: 627 return localStrings.getString('status_interrupted'); 628 case Download.States.COMPLETE: 629 return ''; 630 } 631 } 632 633 /** 634 * Tells the backend to initiate a drag, allowing users to drag 635 * files from the download page and have them appear as native file 636 * drags. 637 */ 638 Download.prototype.drag_ = function() { 639 chrome.send('drag', [this.id_.toString()]); 640 return false; 641 } 642 643 /** 644 * Tells the backend to open this file. 645 */ 646 Download.prototype.openFile_ = function() { 647 chrome.send('openFile', [this.id_.toString()]); 648 return false; 649 } 650 651 /** 652 * Tells the backend that the user chose to save a dangerous file. 653 */ 654 Download.prototype.saveDangerous_ = function() { 655 chrome.send('saveDangerous', [this.id_.toString()]); 656 return false; 657 } 658 659 /** 660 * Tells the backend that the user chose to discard a dangerous file. 661 */ 662 Download.prototype.discardDangerous_ = function() { 663 chrome.send('discardDangerous', [this.id_.toString()]); 664 downloads.remove(this.id_); 665 return false; 666 } 667 668 /** 669 * Tells the backend to show the file in explorer. 670 */ 671 Download.prototype.show_ = function() { 672 chrome.send('show', [this.id_.toString()]); 673 return false; 674 } 675 676 /** 677 * Tells the backend to pause this download. 678 */ 679 Download.prototype.togglePause_ = function() { 680 chrome.send('togglepause', [this.id_.toString()]); 681 return false; 682 } 683 684 /** 685 * Tells the backend to remove this download from history and download shelf. 686 */ 687 Download.prototype.remove_ = function() { 688 chrome.send('remove', [this.id_.toString()]); 689 return false; 690 } 691 692 /** 693 * Tells the backend to cancel this download. 694 */ 695 Download.prototype.cancel_ = function() { 696 chrome.send('cancel', [this.id_.toString()]); 697 return false; 698 } 699 700 /////////////////////////////////////////////////////////////////////////////// 701 // Page: 702 var downloads, localStrings, resultsTimeout; 703 704 function load() { 705 localStrings = new LocalStrings(); 706 downloads = new Downloads(); 707 $('term').focus(); 708 setSearch(''); 709 } 710 711 function setSearch(searchText) { 712 downloads.clear(); 713 downloads.setSearchText(searchText); 714 chrome.send('getDownloads', [searchText.toString()]); 715 } 716 717 function clearAll() { 718 downloads.clear(); 719 downloads.setSearchText(''); 720 chrome.send('clearAll', []); 721 return false; 722 } 723 724 /////////////////////////////////////////////////////////////////////////////// 725 // Chrome callbacks: 726 /** 727 * Our history system calls this function with results from searches or when 728 * downloads are added or removed. 729 */ 730 function downloadsList(results) { 731 if (resultsTimeout) 732 clearTimeout(resultsTimeout); 733 window.console.log('results'); 734 downloads.clear(); 735 downloadUpdated(results); 736 downloads.updateSummary(); 737 } 738 739 /** 740 * When a download is updated (progress, state change), this is called. 741 */ 742 function downloadUpdated(results) { 743 // Sometimes this can get called too early. 744 if (!downloads) 745 return; 746 747 var start = Date.now(); 748 for (var i = 0; i < results.length; i++) { 749 downloads.updated(results[i]); 750 // Do as much as we can in 50ms. 751 if (Date.now() - start > 50) { 752 clearTimeout(resultsTimeout); 753 resultsTimeout = setTimeout(downloadUpdated, 5, results.slice(i + 1)); 754 break; 755 } 756 } 757 } 758 759 </script> 760 </head> 761 <body onload="load();" i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> 762 <div class="header"> 763 <a href="" onclick="setSearch(''); return false;"> 764 <img src="shared/images/downloads_section.png" 765 width="67" height="67" class="logo" border="0" /></a> 766 <form method="post" action="" 767 onsubmit="setSearch(this.term.value); return false;" 768 class="form"> 769 <input type="text" name="term" id="term" /> 770 <input type="submit" name="submit" i18n-values="value:searchbutton" /> 771 </form> 772 </div> 773 <div class="main"> 774 <div id="downloads-summary"> 775 <span id="downloads-summary-text" i18n-content="downloads">Downloads</span> 776 <a id="clear-all" href="" onclick="clearAll();" i18n-content="clear_all">Clear All</a> 777 </div> 778 <div id="downloads-display"></div> 779 </div> 780 <div class="footer"> 781 </div> 782 </body> 783 </html> 784