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