<!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>