| <!DOCTYPE HTML> |
| <html i18n-values="dir:textdirection;"> |
| <head> |
| <meta charset="utf-8"> |
| <title i18n-content="title"></title> |
| <link rel="icon" href="../../app/theme/downloads_favicon.png"> |
| <style> |
| body { |
| background-color: white; |
| color: black; |
| margin: 10px; |
| } |
| |
| .header { |
| overflow: auto; |
| clear: both; |
| } |
| |
| .header .logo { |
| float: left; |
| } |
| |
| .header .form { |
| float: left; |
| margin-top: 22px; |
| -webkit-margin-start: 12px; |
| } |
| |
| html[dir=rtl] .logo, html[dir=rtl] .form { |
| float: right; |
| } |
| |
| #downloads-summary { |
| margin-top: 12px; |
| border-top: 1px solid #9cc2ef; |
| background-color: #ebeff9; |
| padding: 3px; |
| margin-bottom: 6px; |
| } |
| |
| #downloads-summary-text { |
| font-weight: bold; |
| } |
| |
| #downloads-summary > a { |
| float: right; |
| } |
| |
| html[dir=rtl] #downloads-summary > a { |
| float: left; |
| } |
| |
| #downloads-display { |
| max-width: 740px; |
| } |
| |
| .download { |
| position: relative; |
| margin-top: 6px; |
| -webkit-margin-start: 114px; |
| -webkit-padding-start: 56px; |
| margin-bottom: 15px; |
| } |
| |
| .date-container { |
| position: absolute; |
| left: -110px; |
| width: 110px; |
| } |
| |
| html[dir=rtl] .date-container { |
| left: auto; |
| right: -110px; |
| } |
| |
| .date-container .since { |
| color: black; |
| } |
| |
| .date-container .date { |
| color: #666; |
| } |
| |
| .download .icon { |
| position: absolute; |
| top: 2px; |
| left: 9px; |
| width: 32px; |
| height: 32px; |
| } |
| |
| html[dir=rtl] .icon { |
| left: auto; |
| right: 9px; |
| } |
| |
| .download.otr > .safe, |
| .download.otr > .show-dangerous { |
| background: url('shared/images/otr_icon_standalone.png') no-repeat 100% 100%; |
| opacity: .66; |
| -webkit-transition: opacity .15s; |
| } |
| |
| html[dir=rtl] .download.otr > .safe, |
| html[dir=rtl] .download.otr > .show-dangerous { |
| background-position: 0% 100%; |
| } |
| |
| .download.otr > .safe:hover, |
| .download.otr > .show-dangerous:hover { |
| opacity: 1; |
| } |
| |
| .progress { |
| position: absolute; |
| top: -6px; |
| left: 0px; |
| width: 48px; |
| height: 48px; |
| } |
| |
| html[dir=rtl] .progress { |
| left: auto; |
| right: 0px; |
| } |
| |
| .progress.background { |
| background: url('../../app/theme/download_progress_background32.png'); |
| } |
| |
| .progress.foreground { |
| background: url('../../app/theme/download_progress_foreground32.png'); |
| } |
| |
| .name { |
| display: none; |
| -webkit-padding-end: 16px; |
| max-width: 450px; |
| word-break: break-all; |
| } |
| |
| .download .status { |
| display: inline; |
| color: #999; |
| white-space: nowrap; |
| } |
| |
| .download .url { |
| color: #080; |
| max-width: 500px; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| .controls a { |
| color: #777; |
| margin-right: 16px; |
| } |
| |
| #downloads-pagination { |
| padding-top: 24px; |
| margin-left: 18px; |
| } |
| |
| .page-navigation { |
| padding: 8px; |
| background-color: #ebeff9; |
| margin-right: 4px; |
| } |
| |
| .footer { |
| height: 24px; |
| } |
| |
| </style> |
| <script src="shared/js/local_strings.js"></script> |
| <script> |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Helper functions |
| function $(o) {return document.getElementById(o);} |
| |
| /** |
| * Sets the display style of a node. |
| */ |
| function showInline(node, isShow) { |
| node.style.display = isShow ? 'inline' : 'none'; |
| } |
| |
| function showInlineBlock(node, isShow) { |
| node.style.display = isShow ? 'inline-block' : 'none'; |
| } |
| |
| /** |
| * Creates an element of a specified type with a specified class name. |
| * @param {String} type The node type. |
| * @param {String} className The class name to use. |
| */ |
| function createElementWithClassName(type, className) { |
| var elm = document.createElement(type); |
| elm.className = className; |
| return elm; |
| } |
| |
| /** |
| * Creates a link with a specified onclick handler and content |
| * @param {String} onclick The onclick handler |
| * @param {String} value The link text |
| */ |
| function createLink(onclick, value) { |
| var link = document.createElement('a'); |
| link.onclick = onclick; |
| link.href = '#'; |
| link.innerHTML = value; |
| return link; |
| } |
| |
| /** |
| * Creates a button with a specified onclick handler and content |
| * @param {String} onclick The onclick handler |
| * @param {String} value The button text |
| */ |
| function createButton(onclick, value) { |
| var button = document.createElement('input'); |
| button.type = 'button'; |
| button.value = value; |
| button.onclick = onclick; |
| return button; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Downloads |
| /** |
| * Class to hold all the information about the visible downloads. |
| */ |
| function Downloads() { |
| this.downloads_ = {}; |
| this.node_ = $('downloads-display'); |
| this.summary_ = $('downloads-summary-text'); |
| this.searchText_ = ''; |
| |
| // Keep track of the dates of the newest and oldest downloads so that we |
| // know where to insert them. |
| this.newestTime_ = -1; |
| } |
| |
| /** |
| * Called when a download has been updated or added. |
| * @param {Object} download A backend download object (see downloads_ui.cc) |
| */ |
| Downloads.prototype.updated = function(download) { |
| var id = download.id; |
| if (!!this.downloads_[id]) { |
| this.downloads_[id].update(download); |
| } else { |
| this.downloads_[id] = new Download(download); |
| // We get downloads in display order, so we don't have to worry about |
| // maintaining correct order - we can assume that any downloads not in |
| // display order are new ones and so we can add them to the top of the |
| // list. |
| if (download.started > this.newestTime_) { |
| this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild); |
| this.newestTime_ = download.started; |
| } else { |
| this.node_.appendChild(this.downloads_[id].node); |
| } |
| this.updateDateDisplay_(); |
| } |
| } |
| |
| /** |
| * Set our display search text. |
| * @param {String} searchText The string we're searching for. |
| */ |
| Downloads.prototype.setSearchText = function(searchText) { |
| this.searchText_ = searchText; |
| } |
| |
| /** |
| * Update the summary block above the results |
| */ |
| Downloads.prototype.updateSummary = function() { |
| if (this.searchText_) { |
| this.summary_.textContent = localStrings.getStringF('searchresultsfor', |
| this.searchText_); |
| } else { |
| this.summary_.innerHTML = localStrings.getString('downloads'); |
| } |
| |
| var hasDownloads = false; |
| for (var i in this.downloads_) { |
| hasDownloads = true; |
| break; |
| } |
| |
| if (!hasDownloads) { |
| this.node_.innerHTML = localStrings.getString('noresults'); |
| } |
| } |
| |
| /** |
| * Update the date visibility in our nodes so that no date is |
| * repeated. |
| */ |
| Downloads.prototype.updateDateDisplay_ = function() { |
| var dateContainers = document.getElementsByClassName('date-container'); |
| var displayed = {}; |
| for (var i = 0, container; container = dateContainers[i]; i++) { |
| var dateString = container.getElementsByClassName('date')[0].innerHTML; |
| if (!!displayed[dateString]) { |
| container.style.display = 'none'; |
| } else { |
| displayed[dateString] = true; |
| container.style.display = 'block'; |
| } |
| } |
| } |
| |
| /** |
| * Remove a download. |
| * @param {Number} id The id of the download to remove. |
| */ |
| Downloads.prototype.remove = function(id) { |
| this.node_.removeChild(this.downloads_[id].node); |
| delete this.downloads_[id]; |
| this.updateDateDisplay_(); |
| } |
| |
| /** |
| * Clear all downloads and reset us back to a null state. |
| */ |
| Downloads.prototype.clear = function() { |
| for (var id in this.downloads_) { |
| this.downloads_[id].clear(); |
| this.remove(id); |
| } |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Download |
| /** |
| * A download and the DOM representation for that download. |
| * @param {Object} download A backend download object (see downloads_ui.cc) |
| */ |
| function Download(download) { |
| // Create DOM |
| this.node = createElementWithClassName('div','download' + |
| (download.otr ? ' otr' : '')); |
| |
| // Dates |
| this.dateContainer_ = createElementWithClassName('div', 'date-container'); |
| this.node.appendChild(this.dateContainer_); |
| |
| this.nodeSince_ = createElementWithClassName('div', 'since'); |
| this.nodeDate_ = createElementWithClassName('div', 'date'); |
| this.dateContainer_.appendChild(this.nodeSince_); |
| this.dateContainer_.appendChild(this.nodeDate_); |
| |
| // Container for all 'safe download' UI. |
| this.safe_ = createElementWithClassName('div', 'safe'); |
| this.safe_.ondragstart = this.drag_.bind(this); |
| this.node.appendChild(this.safe_); |
| |
| if (download.state != Download.States.COMPLETE) { |
| this.nodeProgressBackground_ = |
| createElementWithClassName('div', 'progress background'); |
| this.safe_.appendChild(this.nodeProgressBackground_); |
| |
| this.canvasProgress_ = |
| document.getCSSCanvasContext('2d', 'canvas_' + download.id, |
| Download.Progress.width, |
| Download.Progress.height); |
| |
| this.nodeProgressForeground_ = |
| createElementWithClassName('div', 'progress foreground'); |
| this.nodeProgressForeground_.style.webkitMask = |
| '-webkit-canvas(canvas_'+download.id+')'; |
| this.safe_.appendChild(this.nodeProgressForeground_); |
| } |
| |
| this.nodeImg_ = createElementWithClassName('img', 'icon'); |
| this.safe_.appendChild(this.nodeImg_); |
| |
| // FileLink is used for completed downloads, otherwise we show FileName. |
| this.nodeTitleArea_ = createElementWithClassName('div', 'title-area'); |
| this.safe_.appendChild(this.nodeTitleArea_); |
| |
| this.nodeFileLink_ = createLink(this.openFile_.bind(this), ''); |
| this.nodeFileLink_.className = 'name'; |
| this.nodeFileLink_.style.display = 'none'; |
| this.nodeTitleArea_.appendChild(this.nodeFileLink_); |
| |
| this.nodeFileName_ = createElementWithClassName('span', 'name'); |
| this.nodeFileName_.style.display = 'none'; |
| this.nodeTitleArea_.appendChild(this.nodeFileName_); |
| |
| this.nodeStatus_ = createElementWithClassName('span', 'status'); |
| this.nodeTitleArea_.appendChild(this.nodeStatus_); |
| |
| this.nodeURL_ = createElementWithClassName('div', 'url'); |
| this.safe_.appendChild(this.nodeURL_); |
| |
| // Controls. |
| this.nodeControls_ = createElementWithClassName('div', 'controls'); |
| this.safe_.appendChild(this.nodeControls_); |
| |
| // We don't need "show in folder" in chromium os. See download_ui.cc and |
| // http://code.google.com/p/chromium-os/issues/detail?id=916. |
| var showinfolder = localStrings.getString('control_showinfolder'); |
| if (showinfolder) { |
| this.controlShow_ = createLink(this.show_.bind(this), showinfolder); |
| this.nodeControls_.appendChild(this.controlShow_); |
| } else { |
| this.controlShow_ = null; |
| } |
| |
| this.controlRetry_ = document.createElement('a'); |
| this.controlRetry_.textContent = localStrings.getString('control_retry'); |
| this.nodeControls_.appendChild(this.controlRetry_); |
| |
| // Pause/Resume are a toggle. |
| this.controlPause_ = createLink(this.togglePause_.bind(this), |
| localStrings.getString('control_pause')); |
| this.nodeControls_.appendChild(this.controlPause_); |
| |
| this.controlResume_ = createLink(this.togglePause_.bind(this), |
| localStrings.getString('control_resume')); |
| this.nodeControls_.appendChild(this.controlResume_); |
| |
| this.controlRemove_ = createLink(this.remove_.bind(this), |
| localStrings.getString('control_removefromlist')); |
| this.nodeControls_.appendChild(this.controlRemove_); |
| |
| this.controlCancel_ = createLink(this.cancel_.bind(this), |
| localStrings.getString('control_cancel')); |
| this.nodeControls_.appendChild(this.controlCancel_); |
| |
| // Container for 'unsafe download' UI. |
| this.danger_ = createElementWithClassName('div', 'show-dangerous'); |
| this.node.appendChild(this.danger_); |
| |
| this.dangerDesc_ = document.createElement('div'); |
| this.danger_.appendChild(this.dangerDesc_); |
| |
| this.dangerSave_ = createButton(this.saveDangerous_.bind(this), |
| localStrings.getString('danger_save')); |
| this.danger_.appendChild(this.dangerSave_); |
| |
| this.dangerDiscard_ = createButton(this.discardDangerous_.bind(this), |
| localStrings.getString('danger_discard')); |
| this.danger_.appendChild(this.dangerDiscard_); |
| |
| // Update member vars. |
| this.update(download); |
| } |
| |
| /** |
| * The states a download can be in. These correspond to states defined in |
| * DownloadsDOMHandler::CreateDownloadItemValue |
| */ |
| Download.States = { |
| IN_PROGRESS : "IN_PROGRESS", |
| CANCELLED : "CANCELLED", |
| COMPLETE : "COMPLETE", |
| PAUSED : "PAUSED", |
| DANGEROUS : "DANGEROUS", |
| INTERRUPTED : "INTERRUPTED", |
| } |
| |
| /** |
| * Explains why a download is in DANGEROUS state. |
| */ |
| Download.DangerType = { |
| NOT_DANGEROUS: "NOT_DANGEROUS", |
| DANGEROUS_FILE: "DANGEROUS_FILE", |
| DANGEROUS_URL: "DANGEROUS_URL", |
| } |
| |
| /** |
| * Constants for the progress meter. |
| */ |
| Download.Progress = { |
| width : 48, |
| height : 48, |
| radius : 24, |
| centerX : 24, |
| centerY : 24, |
| base : -0.5 * Math.PI, |
| dir : false, |
| } |
| |
| /** |
| * Updates the download to reflect new data. |
| * @param {Object} download A backend download object (see downloads_ui.cc) |
| */ |
| Download.prototype.update = function(download) { |
| this.id_ = download.id; |
| this.filePath_ = download.file_path; |
| this.fileName_ = download.file_name; |
| this.url_ = download.url; |
| this.state_ = download.state; |
| this.dangerType_ = download.danger_type; |
| |
| this.since_ = download.since_string; |
| this.date_ = download.date_string; |
| |
| // See DownloadItem::PercentComplete |
| this.percent_ = Math.max(download.percent, 0); |
| this.progressStatusText_ = download.progress_status_text; |
| this.received_ = download.received; |
| |
| if (this.state_ == Download.States.DANGEROUS) { |
| if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) { |
| this.dangerDesc_.innerHTML = localStrings.getStringF('danger_file_desc', |
| this.fileName_); |
| } else { |
| this.dangerDesc_.innerHTML = localStrings.getString('danger_url_desc'); |
| } |
| this.danger_.style.display = 'block'; |
| this.safe_.style.display = 'none'; |
| } else { |
| this.nodeImg_.src = 'chrome://fileicon/' + this.filePath_; |
| |
| if (this.state_ == Download.States.COMPLETE) { |
| this.nodeFileLink_.innerHTML = this.fileName_; |
| this.nodeFileLink_.href = this.filePath_; |
| } else { |
| this.nodeFileName_.innerHTML = this.fileName_; |
| } |
| |
| showInline(this.nodeFileLink_, this.state_ == Download.States.COMPLETE); |
| // nodeFileName_ has to be inline-block to avoid the 'interaction' with |
| // nodeStatus_. If both are inline, it appears that their text contents |
| // are merged before the bidi algorithm is applied leading to an |
| // undesirable reordering. http://crbug.com/13216 |
| showInlineBlock(this.nodeFileName_, this.state_ != Download.States.COMPLETE); |
| |
| if (this.state_ == Download.States.IN_PROGRESS) { |
| this.nodeProgressForeground_.style.display = 'block'; |
| this.nodeProgressBackground_.style.display = 'block'; |
| |
| // Draw a pie-slice for the progress. |
| this.canvasProgress_.clearRect(0, 0, |
| Download.Progress.width, |
| Download.Progress.height); |
| this.canvasProgress_.beginPath(); |
| this.canvasProgress_.moveTo(Download.Progress.centerX, |
| Download.Progress.centerY); |
| |
| // Draw an arc CW for both RTL and LTR. http://crbug.com/13215 |
| this.canvasProgress_.arc(Download.Progress.centerX, |
| Download.Progress.centerY, |
| Download.Progress.radius, |
| Download.Progress.base, |
| Download.Progress.base + Math.PI * 0.02 * |
| Number(this.percent_), |
| false); |
| |
| this.canvasProgress_.lineTo(Download.Progress.centerX, |
| Download.Progress.centerY); |
| this.canvasProgress_.fill(); |
| this.canvasProgress_.closePath(); |
| } else if (this.nodeProgressBackground_) { |
| this.nodeProgressForeground_.style.display = 'none'; |
| this.nodeProgressBackground_.style.display = 'none'; |
| } |
| |
| if (this.controlShow_) { |
| showInline(this.controlShow_, this.state_ == Download.States.COMPLETE); |
| } |
| showInline(this.controlRetry_, this.state_ == Download.States.CANCELLED); |
| this.controlRetry_.href = this.url_; |
| showInline(this.controlPause_, this.state_ == Download.States.IN_PROGRESS); |
| showInline(this.controlResume_, this.state_ == Download.States.PAUSED); |
| var showCancel = this.state_ == Download.States.IN_PROGRESS || |
| this.state_ == Download.States.PAUSED; |
| showInline(this.controlCancel_, showCancel); |
| showInline(this.controlRemove_, !showCancel); |
| |
| this.nodeSince_.innerHTML = this.since_; |
| this.nodeDate_.innerHTML = this.date_; |
| // Don't unnecessarily update the url, as doing so will remove any |
| // text selection the user has started (http://crbug.com/44982). |
| if (this.nodeURL_.textContent != this.url_) |
| this.nodeURL_.textContent = this.url_; |
| this.nodeStatus_.innerHTML = this.getStatusText_(); |
| |
| this.danger_.style.display = 'none'; |
| this.safe_.style.display = 'block'; |
| } |
| } |
| |
| /** |
| * Removes applicable bits from the DOM in preparation for deletion. |
| */ |
| Download.prototype.clear = function() { |
| this.safe_.ondragstart = null; |
| this.nodeFileLink_.onclick = null; |
| if (this.controlShow_) { |
| this.controlShow_.onclick = null; |
| } |
| this.controlCancel_.onclick = null; |
| this.controlPause_.onclick = null; |
| this.controlResume_.onclick = null; |
| this.dangerDiscard_.onclick = null; |
| |
| this.node.innerHTML = ''; |
| } |
| |
| /** |
| * @return {String} User-visible status update text. |
| */ |
| Download.prototype.getStatusText_ = function() { |
| switch (this.state_) { |
| case Download.States.IN_PROGRESS: |
| return this.progressStatusText_; |
| case Download.States.CANCELLED: |
| return localStrings.getString('status_cancelled'); |
| case Download.States.PAUSED: |
| return localStrings.getString('status_paused'); |
| case Download.States.DANGEROUS: |
| var desc = this.dangerType_ == Download.DangerType.DANGEROUS_FILE ? |
| 'danger_file_desc' : 'danger_url_desc'; |
| return localStrings.getString(desc); |
| case Download.States.INTERRUPTED: |
| return localStrings.getString('status_interrupted'); |
| case Download.States.COMPLETE: |
| return ''; |
| } |
| } |
| |
| /** |
| * Tells the backend to initiate a drag, allowing users to drag |
| * files from the download page and have them appear as native file |
| * drags. |
| */ |
| Download.prototype.drag_ = function() { |
| chrome.send('drag', [this.id_.toString()]); |
| return false; |
| } |
| |
| /** |
| * Tells the backend to open this file. |
| */ |
| Download.prototype.openFile_ = function() { |
| chrome.send('openFile', [this.id_.toString()]); |
| return false; |
| } |
| |
| /** |
| * Tells the backend that the user chose to save a dangerous file. |
| */ |
| Download.prototype.saveDangerous_ = function() { |
| chrome.send('saveDangerous', [this.id_.toString()]); |
| return false; |
| } |
| |
| /** |
| * Tells the backend that the user chose to discard a dangerous file. |
| */ |
| Download.prototype.discardDangerous_ = function() { |
| chrome.send('discardDangerous', [this.id_.toString()]); |
| downloads.remove(this.id_); |
| return false; |
| } |
| |
| /** |
| * Tells the backend to show the file in explorer. |
| */ |
| Download.prototype.show_ = function() { |
| chrome.send('show', [this.id_.toString()]); |
| return false; |
| } |
| |
| /** |
| * Tells the backend to pause this download. |
| */ |
| Download.prototype.togglePause_ = function() { |
| chrome.send('togglepause', [this.id_.toString()]); |
| return false; |
| } |
| |
| /** |
| * Tells the backend to remove this download from history and download shelf. |
| */ |
| Download.prototype.remove_ = function() { |
| chrome.send('remove', [this.id_.toString()]); |
| return false; |
| } |
| |
| /** |
| * Tells the backend to cancel this download. |
| */ |
| Download.prototype.cancel_ = function() { |
| chrome.send('cancel', [this.id_.toString()]); |
| return false; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Page: |
| var downloads, localStrings, resultsTimeout; |
| |
| function load() { |
| localStrings = new LocalStrings(); |
| downloads = new Downloads(); |
| $('term').focus(); |
| setSearch(''); |
| } |
| |
| function setSearch(searchText) { |
| downloads.clear(); |
| downloads.setSearchText(searchText); |
| chrome.send('getDownloads', [searchText.toString()]); |
| } |
| |
| function clearAll() { |
| downloads.clear(); |
| downloads.setSearchText(''); |
| chrome.send('clearAll', []); |
| return false; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Chrome callbacks: |
| /** |
| * Our history system calls this function with results from searches or when |
| * downloads are added or removed. |
| */ |
| function downloadsList(results) { |
| if (resultsTimeout) |
| clearTimeout(resultsTimeout); |
| window.console.log('results'); |
| downloads.clear(); |
| downloadUpdated(results); |
| downloads.updateSummary(); |
| } |
| |
| /** |
| * When a download is updated (progress, state change), this is called. |
| */ |
| function downloadUpdated(results) { |
| // Sometimes this can get called too early. |
| if (!downloads) |
| return; |
| |
| var start = Date.now(); |
| for (var i = 0; i < results.length; i++) { |
| downloads.updated(results[i]); |
| // Do as much as we can in 50ms. |
| if (Date.now() - start > 50) { |
| clearTimeout(resultsTimeout); |
| resultsTimeout = setTimeout(downloadUpdated, 5, results.slice(i + 1)); |
| break; |
| } |
| } |
| } |
| |
| </script> |
| </head> |
| <body onload="load();" i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> |
| <div class="header"> |
| <a href="" onclick="setSearch(''); return false;"> |
| <img src="shared/images/downloads_section.png" |
| width="67" height="67" class="logo" border="0" /></a> |
| <form method="post" action="" |
| onsubmit="setSearch(this.term.value); return false;" |
| class="form"> |
| <input type="text" name="term" id="term" /> |
| <input type="submit" name="submit" i18n-values="value:searchbutton" /> |
| </form> |
| </div> |
| <div class="main"> |
| <div id="downloads-summary"> |
| <span id="downloads-summary-text" i18n-content="downloads">Downloads</span> |
| <a id="clear-all" href="" onclick="clearAll();" i18n-content="clear_all">Clear All</a> |
| </div> |
| <div id="downloads-display"></div> |
| </div> |
| <div class="footer"> |
| </div> |
| </body> |
| </html> |