<!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 type="text/css"> *:-khtml-drag { background-color: rgba(238,238,238, 0.5); } *[draggable] { -khtml-user-drag: element; cursor: move; } ul.downloadlist { list-style-type: none; margin: 0; padding: 0; position: relative; } .menuicon { margin: 20px auto auto auto; height: 4px; width: 5px; background: url('shared/images/active_downloads_menu.png') no-repeat; } .menubutton { float: right; height: 43px; width: 24px; border-left: 1px solid #CCC; border-bottom: 1px solid #CCC; cursor: pointer; } /* TODO(achuith): fix rtl */ html[dir=rtl] .menubutton { float: left; } .rowbg { border-bottom: 1px solid #CCC; background: -webkit-gradient(linear, left top, left bottom, from(#f3f3f3), to(#ebebeb), color-stop(0.8, #ededed)); } .rowbg:hover { background: -webkit-gradient(linear, left top, left bottom, from(#fdfdfd), to(#f1f1f1), color-stop(0.8, #f5f5f5)); } .rowbg:active { background: -webkit-gradient(linear, left top, left bottom, from(#d2e0f0), to(#dee6f0), color-stop(0.1, #e3e9f0)); } .downloadrow { height: 36px; } .rowbutton { padding: 5px 5px 0 7px; position: relative; width:213px; height: 30px; } .rowbutton div.icon { float: left; margin-top: 1px; width: 21px; height: 17px; background-repeat: no-repeat; } .rowbutton div.iconwarning { float: left; margin-top: 1px; width: 37px; height: 30px; background: url('shared/images/icon_warning2.png') no-repeat; } .rowbutton span.title { position: relative; text-overflow: ellipsis; white-space: nowrap; display: inline-block; overflow: hidden; width: 191px; margin-right: -20px; color: #325282; font-size: .8em; } .dangerousprompt { margin: 0 5px 5px 3px; display: block; } .dangerousprompttext { font-size: .7em; color: #325282; } .confirm { font-size: .6em; text-decoration: underline; margin-left: 5px; color: #254f9b; cursor: pointer; } .progress { font-size: .7em; text-align: right; color: #325282; margin-top: 2px; } div.columnlist { width: 100%; top: 0; left: 0; bottom: 29px; position: absolute; background: #e8e8e8 } span.showalldownloadstext { color: #254f9b; cursor: pointer; text-decoration: underline; font-size: 12px; height: 15px; } div.showalldownloads { width: 100%; bottom: 0; height: 29px; position: absolute; margin-left: -8px; text-align: center; background: #e8e8e8 } .menu { margin-top: 5px; right: 15px; border-bottom-left-radius: 4px 4px; border-bottom-right-radius: 4px 4px; border-top-left-radius: 4px 4px; border-top-right-radius: 0px 0px; position: absolute; display: none; z-index: 999; background: -webkit-gradient(linear, left top, left bottom, from(#f3f3f3), to(#ebebeb), color-stop(0.8, #ededed)); border: 1px solid rgba(0, 0, 0, 0.6); padding: 2px 5px 0 5px; } .menuitem { width: 100%; height: 20px; font-size: .8em; text-align: left; cursor: pointer; left: 0; color: #0D0052; -webkit-transition: color 1.0s ease-out ; } .menuitem:hover { text-decoration: underline; color: #20c; background: #ebeff9; -webkit-transition: color 0.0s ease-out ; } </style> <script src="shared/js/local_strings.js"></script> <script src="shared/js/media_common.js"></script> <script src="shared/js/util.js"></script> <script> /** * Wrapper for chrome.send. */ function chromeSend(func, arg) { if (arg == undefined) arg = ''; // Convert to string. if (typeof arg == 'number') arg = '' + arg; chrome.send(func, [arg]); }; /** * Create a child element. * * @param {string} type The type - div, span, etc. * @param {string} className The class name * @param {HTMLElement} parent Parent to append this child to. * @param {string} textContent optional text content of child. * @param {function(*)} onclick onclick function of child. */ function createChild(type, className, parent, textContent, onclick) { var elem = document.createElement(type); elem.className = className; if (textContent !== undefined) elem.textContent = textContent; elem.onclick = onclick; parent.appendChild(elem); return elem; }; var localStrings; var downloadRowList; function init() { localStrings = new LocalStrings(); initTestHarness(); window.onkeydown = function(e) { if (e.keyCode == 27) // Escape. menu.clear(); e.preventDefault(); // Suppress browser shortcuts. }; $('showalldownloadstext').textContent = localStrings.getString('showalldownloads'); downloadRowList = new DownloadRowList(); chromeSend('getDownloads'); } /** * Testing. Allow this page to be loaded in a browser. * Create stubs for localStrings and chrome.send. */ function initTestHarness() { if (location.protocol != 'file:') return; // Enable right click for dom inspector. document.body.oncontextmenu = ''; // Fix localStrings. localStrings = { getString: function(name) { if (name == 'showalldownloads') return 'Show All Downloads'; if (name == 'dangerousextension') return 'Extensions, apps, and themes can harm your computer.' + ' Are you sure you want to continue?' if (name == 'continue') return 'Continue'; if (name == 'discard') return 'Discard'; return name; }, getStringF: function(name, path) { return path + ' - Unknown file type.'; }, }; // Log chrome.send calls. chrome.send = function(name, ary) { console.log('chrome.send ' + name + ' ' + ary); if (name == 'getDownloads' || (name == 'openNewFullWindow' && ary[0] == 'chrome://downloads')) sendTestResults(); }; // Fix resource images. var cssRules = document.styleSheets[0].cssRules; for (var i = 0; i < cssRules.length; i++) { var cssRule = cssRules[i]; if (cssRule.selectorText.match(/^div\.icon|^\.menuicon/)) { cssRule.style.backgroundImage = cssRule.style.backgroundImage.replace('chrome://resources', 'shared'); } } } /** * Create a results array with test data and call downloadsList. */ var testElement; var testId = 0; var testResults = []; function sendTestResults() { var testState = (testId % 3 == 0 ? 'IN_PROGRESS' : (testId % 3 == 1 ? 'DANGEROUS' : 'COMPLETE')); state1 = (testId % 3 == 0); testResults.push({ state: testState, percent: (testId % 3 == 0 ? 90 : 100), id: testId, file_name: ' Test' + testId + '.pdf', file_path: '/home/achuith/Downloads/Test' + testId + '.pdf', progress_status_text : '107 MB/s - 108 MB of 672 MB, 5 secs left', }); testId++; downloadsList(testResults); } /** * Current Menu. */ var menu = { current_: null, /** * Close the current menu. */ clear: function() { var current = this.current_; if (current) { current.firstChild.style.display = 'none'; current.style.opacity = ''; this.current_ = null; } }, /** * If it's a second click on an open menu, close the menu. * Otherwise, close any other open menu and open the clicked menu. */ clicked: function(row) { var menuicon = row.menuicon; if (this.current_ === menuicon) { this.clear(); return; } this.clear(); if (menuicon.firstChild.style.display != 'block') { menuicon.firstChild.style.display = 'block'; menuicon.style.opacity = '1'; menuicon.scrollIntoView(); this.current_ = menuicon; } window.event.stopPropagation(); }, }; function DiscardResult(result) { return (result.state == 'CANCELLED' || result.state == 'INTERRUPTED' || result.state == 'REMOVING'); }; /** * C++ api calls. */ function downloadsList(results) { downloadRowList.list(results); } function downloadUpdated(result) { downloadRowList.update(result); } function showAllDownloads() { chromeSend('openNewFullWindow', 'chrome://downloads'); } /** * DownloadRow contains all the elements that go into a row of the downloads * list. It represents a single DownloadItem. * * @param {DownloadRowList} list Global DownloadRowList. * @param {Object} result JSON representation of DownloadItem. * @constructor */ function DownloadRow(list, result) { this.path = result.file_path; this.name = result.file_name; this.fileUrl = result.file_url; this.list = list; this.id = result.id; this.createRow_(list); this.createMenu_(); this.createRowButton_(); this.setMenuHidden_(true); } DownloadRow.prototype = { /** * Create the row html element and book-keeping for the row. * @param {DownloadRowList} list global DownloadRowList instance. * @private */ createRow_: function(list) { var elem = document.createElement('li'); elem.className = 'downloadrow'; elem.id = this.path; elem.row = this; this.element = elem; list.append(this); }, setErrorText_: function(text) { this.filename.textContent = text; }, supportsPdf_: function() { return 'application/pdf' in navigator.mimeTypes; }, openFilePath_: function() { chromeSend('openNewFullWindow', this.fileUrl); }, /** * Determine onclick behavior based on filename. * @private */ getFunctionForItem_: function() { var path = this.path; var self = this; if (pathIsAudioFile(path)) { return function() { chromeSend('playMediaFile', path); }; } if (pathIsVideoFile(path)) { return function() { chromeSend('playMediaFile', path); }; } if (pathIsImageFile(path)) { return function() { self.openFilePath_(); } } if (pathIsHtmlFile(path)) { return function() { self.openFilePath_(); } } if (pathIsPdfFile(path) && this.supportsPdf_()) { return function() { self.openFilePath_(); } } return function() { self.setErrorText_(localStrings.getStringF('error_unknown_file_type', self.name)); }; }, setDangerousIcon_: function(warning) { this.icon.className = warning ? 'iconwarning' : 'icon'; this.icon.style.background = warning ? '' : 'url(chrome://fileicon' + escape(this.path) + '?iconsize=small) no-repeat'; }, /** * Create the row button for the left of the row. * This contains the icon, filename and error elements. * @private */ createRowButton_: function () { this.rowbutton = createChild('div', 'rowbutton rowbg', this.element); // Icon. this.icon = createChild('div', 'icon', this.rowbutton); this.setDangerousIcon_(false); // Filename. this.filename = createChild('span', 'title', this.rowbutton, this.name); }, setMenuHidden_: function(hidden) { this.menubutton.hidden = hidden; if (hidden) { this.rowbutton.style.width = '238px'; } else { this.rowbutton.style.width = ''; } }, /** * Create the menu button on the right of the row. * This contains the menuicon. The menuicon contains the menu, which * contains items for Pause/Resume and Cancel. * @private */ createMenu_: function() { var self = this; this.menubutton = createChild('div', 'menubutton rowbg', this.element, '', function() { menu.clicked(self); }); this.menuicon = createChild('div', 'menuicon', this.menubutton); var menudiv = createChild('div', 'menu', this.menuicon); this.pause = createChild('div', 'menuitem', menudiv, localStrings.getString('pause'), function() { self.pauseToggleDownload_(); }); this.cancel = createChild('div', 'menuitem', menudiv, localStrings.getString('cancel'), function() { self.cancelDownload_(); }); }, allowDownload_: function() { chromeSend('allowDownload', this.id); }, cancelDownload_: function() { chromeSend('cancelDownload', this.id); }, pauseToggleDownload_: function() { this.pause.textContent = (this.pause.textContent == localStrings.getString('pause')) ? localStrings.getString('resume') : localStrings.getString('pause'); chromeSend('pauseToggleDownload', this.id); }, changeElemHeight_: function(elem, x) { elem.style.height = elem.clientHeight + x + 'px'; }, changeRowHeight_: function(x) { this.list.rowsHeight += x; this.changeElemHeight_(this.element, x); // rowbutton has 5px padding. this.changeElemHeight_(this.rowbutton, x - 5); this.list.resize(); }, DANGEROUS_HEIGHT: 60, createDangerousPrompt_: function(dangerType) { if (this.dangerous) return; this.dangerous = createChild('div', 'dangerousprompt', this.rowbutton); // Handle dangerous files, extensions and dangerous urls. var dangerText; if (dangerType == 'DANGEROUS_URL') { dangerText = localStrings.getString('dangerousurl'); } else if (dangerType == 'DANGEROUS_FILE' && this.path.match(/\.crx$/)) { dangerText = localStrings.getString('dangerousextension'); } else { dangerText = localStrings.getStringF('dangerousfile', this.name); } createChild('span', 'dangerousprompttext', this.dangerous, dangerText); var self = this; createChild('span', 'confirm', this.dangerous, localStrings.getString('discard'), function() { self.cancelDownload_(); }); createChild('span', 'confirm', this.dangerous, localStrings.getString('continue'), function() { self.allowDownload_(); }); this.changeRowHeight_(this.DANGEROUS_HEIGHT); this.setDangerousIcon_(true); }, removeDangerousPrompt_: function() { if (!this.dangerous) return; this.rowbutton.removeChild(this.dangerous); this.dangerous = null; this.changeRowHeight_(-this.DANGEROUS_HEIGHT); this.setDangerousIcon_(false); }, PROGRESS_HEIGHT: 8, createProgress_: function() { if (this.progress) return; this.progress = createChild('div', 'progress', this.rowbutton); this.setMenuHidden_(false); this.changeRowHeight_(this.PROGRESS_HEIGHT); }, removeProgress_: function() { if (!this.progress) return; this.rowbutton.removeChild(this.progress); this.progress = null; this.changeRowHeight_(-this.PROGRESS_HEIGHT); this.setMenuHidden_(true); }, updatePause_: function(result) { var pause = this.pause; var pauseStr = localStrings.getString('pause'); var resumeStr = localStrings.getString('resume'); if (pause && result.state == 'PAUSED' && pause.textContent != resumeStr) { pause.textContent = resumeStr; } else if (pause && result.state == 'IN_PROGRESS' && pause.textContent != pauseStr) { pause.textContent = pauseStr; } }, progressStatusText_: function(progress) { if (!progress) return progress; /* m looks like this: ["107 MB/s - 108 MB of 672 MB, 5 secs left", "107 MB/s", "108", "MB", "672", "MB", "5 secs left"] We want to return progress text like this: "108 / 672 MB, 5 secs left" or "108 kB / 672 MB, 5 secs left" */ var m = progress.match( /([^-]*) - ([0-9\.]*) ([a-zA-Z]*) of ([0-9\.]*) ([a-zA-Z]*), (.*)/); if (!m || m.length != 7) return progress; return m[2] + (m[3] == m[5] ? '' : ' ' + m[3]) + ' / ' + m[4] + ' ' + m[5] + ', ' + m[6]; }, updateProgress_: function(result) { this.removeDangerousPrompt_(); this.createProgress_(); this.progress.textContent = this.progressStatusText_(result.progress_status_text); this.updatePause_(result); }, /** * Called when the item has finished downloading. Switch the menu * and remove the progress bar. * @private */ finishedDownloading_: function() { // Make rowbutton clickable. this.rowbutton.onclick = this.getFunctionForItem_(); this.rowbutton.style.cursor = 'pointer'; // Make rowbutton draggable. this.rowbutton.setAttribute('draggable', 'true'); var self = this; this.rowbutton.addEventListener('dragstart', function(e) { e.dataTransfer.effectAllowed = 'copy'; e.dataTransfer.setData('Text', self.path); e.dataTransfer.setData('URL', self.fileUrl); }, false); this.removeDangerousPrompt_(); this.removeProgress_(); }, /** * One of the DownloadItem we are observing has updated. * @param {Object} result JSON representation of DownloadItem. */ update: function(result) { this.filename.textContent = result.file_name; this.id = result.id; if (result.state != 'COMPLETE') { this.rowbutton.onclick = ''; this.rowbutton.style.cursor = ''; } if (DiscardResult(result)) { this.list.remove(this); } else if (result.state == 'DANGEROUS') { this.createDangerousPrompt_(result.danger_type); } else if (result.percent < 100) { this.updateProgress_(result); } else if (result.state == 'COMPLETE') { this.finishedDownloading_(); } }, }; /** * DownloadRowList is a container for DownloadRows. */ function DownloadRowList() { this.element = createChild('ul', 'downloadlist', $('main')); document.title = localStrings.getString('downloadpath'). split('/').pop(); } DownloadRowList.prototype = { /** * numRows is the current number of rows. * rowsHeight is the sum of the heights of all rows. * rowListHeight is the height of the container containing the rows. * rows is the list of DownloadRows. */ numRows: 0, rowsHeight: 0, rowListHeight: 72, rows: [], /** * Resize the panel to accomodate all rows. */ resize: function() { var diff = this.rowsHeight - this.rowListHeight; if (diff != 0 && (this.rowListHeight + diff > 72)) { window.resizeBy(0, diff); this.rowListHeight += diff; } }, /** * Remove a row from the list, as when a download is canceled, or * the the number of rows has exceeded the max allowed. * * @param {DownloadRow} row Row to be removed. * @private */ remove: function(row) { this.rows.splice(this.rows.indexOf(row), 1); this.numRows--; this.rowsHeight -= row.element.offsetHeight; this.resize(); this.element.removeChild(row.element); row.element.row = null; }, removeList: function(rows) { for (i = 0; i < rows.length; i++) { this.remove(rows[i]); } }, updateList: function(results) { for (var i = 0; i < results.length; i++) { this.update(results[i]); } }, /** * Append a new row to the list, removing the last row if we exceed the * maximum allowed. * @param {DownloadRow} row Row to be removed. */ append: function(row) { this.rows.push(row); var elem = row.element; var list = this.element; if (list.firstChild) { list.insertBefore(elem, list.firstChild); } else { list.appendChild(elem); } this.rowsHeight += elem.offsetHeight; this.numRows++; // We display no more than 5 elements. if (this.numRows > 5) this.remove(list.lastChild.row); this.resize(); }, getRow: function(path) { for (var i = 0; i < this.rows.length; i++) { if (this.rows[i].path == path) return this.rows[i]; } }, /** * Returns the list of rows that are not in the results array. * @param {Array} results Array of JSONified DownloadItems. */ findMissing: function(results) { var removeList = []; for (var i = 0; i < this.rows.length; i++) { var row = this.rows[i]; var found = false; for (var j = 0; j < results.length; j++) { if (row.path == results[j].file_path) { found = true; break; } } if (!found) removeList.push(row); } return removeList; }, /** * Handle list callback with list of DownloadItems. * @param {Array} results Array of JSONified DownloadItems. */ list: function(results) { var rows = this.findMissing(results); this.updateList(results); this.removeList(rows); }, /** * Handle update of a DownloadItem we're observing. * @param {Object} result JSON representation of DownloadItem. */ update: function(result) { var row = this.getRow(result.file_path); if (!row && !DiscardResult(result)) row = new DownloadRow(this, result); row && row.update(result); }, }; </script> <body onload="init();" onclick="menu.clear();" onselectstart="return false;" i18n-values=".style.fontFamily:fontfamily" oncontextmenu="return false;" onBlur="menu.clear();"> <div id="main" class="columnlist"></div> <div id="showalldownloads" class="showalldownloads"> <span id="showalldownloadstext" class="showalldownloadstext" onclick="showAllDownloads()"></span> </div> </body> </html>