| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 // TODO(hcarmona): This file is big: it may be good to split it up. | |
| 6 | |
| 7 /** | |
| 8 * The type of the download object. The definition is based on | |
| 9 * chrome/browser/ui/webui/downloads_dom_handler.cc:CreateDownloadItemValue() | |
| 10 * @typedef {{by_ext_id: (string|undefined), | |
| 11 * by_ext_name: (string|undefined), | |
| 12 * danger_type: (string|undefined), | |
| 13 * date_string: string, | |
| 14 * file_externally_removed: boolean, | |
| 15 * file_name: string, | |
| 16 * file_path: string, | |
| 17 * file_url: string, | |
| 18 * id: string, | |
| 19 * last_reason_text: (string|undefined), | |
| 20 * otr: boolean, | |
| 21 * percent: (number|undefined), | |
| 22 * progress_status_text: (string|undefined), | |
| 23 * received: (number|undefined), | |
| 24 * resume: boolean, | |
| 25 * retry: boolean, | |
| 26 * since_string: string, | |
| 27 * started: number, | |
| 28 * state: string, | |
| 29 * total: number, | |
| 30 * url: string}} | |
| 31 */ | |
| 32 var DownloadItem; | |
| 33 | |
| 34 /** | |
| 35 * Creates a link with a specified onclick handler and content. | |
| 36 * @param {function()} onclick The onclick handler. | |
| 37 * @param {string=} opt_text The link text. | |
| 38 * @return {!Element} The created link element. | |
| 39 */ | |
| 40 function createActionLink(onclick, opt_text) { | |
| 41 var link = new ActionLink; | |
| 42 link.onclick = onclick; | |
| 43 if (opt_text) link.textContent = opt_text; | |
| 44 return link; | |
| 45 } | |
| 46 | |
| 47 /** | |
| 48 * Creates a button with a specified onclick handler and content. | |
| 49 * @param {function()} onclick The onclick handler. | |
| 50 * @param {string} value The button text. | |
| 51 * @return {Element} The created button. | |
| 52 */ | |
| 53 function createButton(onclick, value) { | |
| 54 var button = document.createElement('input'); | |
| 55 button.type = 'button'; | |
| 56 button.value = value; | |
| 57 button.onclick = onclick; | |
| 58 return button; | |
| 59 } | |
| 60 | |
| 61 /////////////////////////////////////////////////////////////////////////////// | |
| 62 // DownloadFocusRow: | |
| 63 | |
| 64 /** | |
| 65 * Provides an implementation for a single column grid. | |
| 66 * @constructor | |
| 67 * @extends {cr.ui.FocusRow} | |
| 68 */ | |
| 69 function DownloadFocusRow() {} | |
| 70 | |
| 71 /** | |
| 72 * Decorates |focusRow| so that it can be treated as a DownloadFocusRow. | |
| 73 * @param {Element} focusRow The element that has all the columns represented | |
| 74 * by |download|. | |
| 75 * @param {Download} download The Download representing this row. | |
| 76 * @param {Node} boundary Focus events are ignored outside of this node. | |
| 77 */ | |
| 78 DownloadFocusRow.decorate = function(focusRow, download, boundary) { | |
| 79 focusRow.__proto__ = DownloadFocusRow.prototype; | |
| 80 focusRow.decorate(boundary); | |
| 81 | |
| 82 // Add all clickable elements as a row into the grid. | |
| 83 focusRow.addElementIfFocusable_(download.nodeFileLink_, 'name'); | |
| 84 focusRow.addElementIfFocusable_(download.nodeURL_, 'url'); | |
| 85 focusRow.addElementIfFocusable_(download.controlShow_, 'show'); | |
| 86 focusRow.addElementIfFocusable_(download.controlRetry_, 'retry'); | |
| 87 focusRow.addElementIfFocusable_(download.controlPause_, 'pause'); | |
| 88 focusRow.addElementIfFocusable_(download.controlResume_, 'resume'); | |
| 89 focusRow.addElementIfFocusable_(download.controlRemove_, 'remove'); | |
| 90 focusRow.addElementIfFocusable_(download.controlCancel_, 'cancel'); | |
| 91 focusRow.addElementIfFocusable_(download.malwareSave_, 'save'); | |
| 92 focusRow.addElementIfFocusable_(download.dangerSave_, 'save'); | |
| 93 focusRow.addElementIfFocusable_(download.malwareDiscard_, 'discard'); | |
| 94 focusRow.addElementIfFocusable_(download.dangerDiscard_, 'discard'); | |
| 95 focusRow.addElementIfFocusable_(download.controlByExtensionLink_, | |
| 96 'extension'); | |
| 97 }; | |
| 98 | |
| 99 DownloadFocusRow.prototype = { | |
| 100 __proto__: cr.ui.FocusRow.prototype, | |
| 101 | |
| 102 /** @override */ | |
| 103 getEquivalentElement: function(element) { | |
| 104 if (this.focusableElements.indexOf(element) > -1) | |
| 105 return element; | |
| 106 | |
| 107 // All elements default to another element with the same type. | |
| 108 var columnType = element.getAttribute('column-type'); | |
| 109 var equivalent = this.querySelector('[column-type=' + columnType + ']'); | |
| 110 | |
| 111 if (this.focusableElements.indexOf(equivalent) < 0) { | |
| 112 var equivalentTypes = | |
| 113 ['show', 'retry', 'pause', 'resume', 'remove', 'cancel']; | |
| 114 if (equivalentTypes.indexOf(columnType) != -1) { | |
| 115 var allTypes = equivalentTypes.map(function(type) { | |
| 116 return '[column-type=' + type + ']:not([hidden])'; | |
| 117 }).join(', '); | |
| 118 equivalent = this.querySelector(allTypes); | |
| 119 } | |
| 120 } | |
| 121 | |
| 122 // Return the first focusable element if no equivalent element is found. | |
| 123 return equivalent || this.focusableElements[0]; | |
| 124 }, | |
| 125 | |
| 126 /** | |
| 127 * @param {Element} element The element that should be added. | |
| 128 * @param {string} type The column type to use for the element. | |
| 129 * @private | |
| 130 */ | |
| 131 addElementIfFocusable_: function(element, type) { | |
| 132 if (this.shouldFocus_(element)) { | |
| 133 this.addFocusableElement(element); | |
| 134 element.setAttribute('column-type', type); | |
| 135 } | |
| 136 }, | |
| 137 | |
| 138 /** | |
| 139 * Determines if element should be focusable. | |
| 140 * @param {Element} element | |
| 141 * @return {boolean} | |
| 142 * @private | |
| 143 */ | |
| 144 shouldFocus_: function(element) { | |
| 145 if (!element) | |
| 146 return false; | |
| 147 | |
| 148 // Hidden elements are not focusable. | |
| 149 var style = window.getComputedStyle(element); | |
| 150 if (style.visibility == 'hidden' || style.display == 'none') | |
| 151 return false; | |
| 152 | |
| 153 // Verify all ancestors are focusable. | |
| 154 return !element.parentElement || this.shouldFocus_(element.parentElement); | |
| 155 }, | |
| 156 }; | |
| 157 | |
| 158 /////////////////////////////////////////////////////////////////////////////// | |
| 159 // Downloads | |
| 160 /** | |
| 161 * Class to hold all the information about the visible downloads. | |
| 162 * @constructor | |
| 163 */ | |
| 164 function Downloads() { | |
| 165 /** | |
| 166 * @type {!Object<string, Download>} | |
| 167 * @private | |
| 168 */ | |
| 169 this.downloads_ = {}; | |
| 170 this.node_ = $('downloads-display'); | |
| 171 this.summary_ = $('downloads-summary-text'); | |
| 172 this.searchText_ = ''; | |
| 173 this.focusGrid_ = new cr.ui.FocusGrid(); | |
| 174 | |
| 175 // Keep track of the dates of the newest and oldest downloads so that we | |
| 176 // know where to insert them. | |
| 177 this.newestTime_ = -1; | |
| 178 | |
| 179 // Icon load request queue. | |
| 180 this.iconLoadQueue_ = []; | |
| 181 this.isIconLoading_ = false; | |
| 182 | |
| 183 this.progressForeground1_ = new Image(); | |
| 184 this.progressForeground1_.src = | |
| 185 'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32@1x'; | |
| 186 this.progressForeground2_ = new Image(); | |
| 187 this.progressForeground2_.src = | |
| 188 'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32@2x'; | |
| 189 | |
| 190 cr.ui.decorate('command', cr.ui.Command); | |
| 191 document.addEventListener('canExecute', this.onCanExecute_.bind(this)); | |
| 192 document.addEventListener('command', this.onCommand_.bind(this)); | |
| 193 } | |
| 194 | |
| 195 /** | |
| 196 * Called when a download has been updated or added. | |
| 197 * @param {DownloadItem} download Information about a download. | |
| 198 */ | |
| 199 Downloads.prototype.updated = function(download) { | |
| 200 var id = download.id; | |
| 201 if (this.downloads_[id]) { | |
| 202 this.downloads_[id].update(download); | |
| 203 } else { | |
| 204 this.downloads_[id] = new Download(download); | |
| 205 // We get downloads in display order, so we don't have to worry about | |
| 206 // maintaining correct order - we can assume that any downloads not in | |
| 207 // display order are new ones and so we can add them to the top of the | |
| 208 // list. | |
| 209 if (download.started > this.newestTime_) { | |
| 210 this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild); | |
| 211 this.newestTime_ = download.started; | |
| 212 } else { | |
| 213 this.node_.appendChild(this.downloads_[id].node); | |
| 214 } | |
| 215 } | |
| 216 // Download.prototype.update may change its nodeSince_ and nodeDate_, so | |
| 217 // update all the date displays. | |
| 218 // TODO(benjhayden) Only do this if its nodeSince_ or nodeDate_ actually did | |
| 219 // change since this may touch 150 elements and Downloads.prototype.updated | |
| 220 // may be called 150 times. | |
| 221 this.onDownloadListChanged_(); | |
| 222 }; | |
| 223 | |
| 224 /** | |
| 225 * Set our display search text. | |
| 226 * @param {string} searchText The string we're searching for. | |
| 227 */ | |
| 228 Downloads.prototype.setSearchText = function(searchText) { | |
| 229 this.searchText_ = searchText; | |
| 230 }; | |
| 231 | |
| 232 /** Update the summary block above the results. */ | |
| 233 Downloads.prototype.updateSummary = function() { | |
| 234 if (this.searchText_) { | |
| 235 this.summary_.textContent = loadTimeData.getStringF('searchresultsfor', | |
| 236 this.searchText_); | |
| 237 } else { | |
| 238 this.summary_.textContent = ''; | |
| 239 } | |
| 240 }; | |
| 241 | |
| 242 /** | |
| 243 * Called when either a search or load completes to update whether there are | |
| 244 * results or not. | |
| 245 */ | |
| 246 Downloads.prototype.updateResults = function() { | |
| 247 var noDownloadsOrResults = $('no-downloads-or-results'); | |
| 248 noDownloadsOrResults.textContent = loadTimeData.getString( | |
| 249 this.searchText_ ? 'no_search_results' : 'no_downloads'); | |
| 250 | |
| 251 var hasDownloads = this.size() > 0; | |
| 252 this.node_.hidden = !hasDownloads; | |
| 253 noDownloadsOrResults.hidden = hasDownloads; | |
| 254 | |
| 255 if (loadTimeData.getBoolean('allow_deleting_history')) | |
| 256 $('clear-all').hidden = !hasDownloads || this.searchText_.length > 0; | |
| 257 | |
| 258 this.rebuildFocusGrid_(); | |
| 259 }; | |
| 260 | |
| 261 /** | |
| 262 * Rebuild the focusGrid_ using the elements that each download will have. | |
| 263 * @private | |
| 264 */ | |
| 265 Downloads.prototype.rebuildFocusGrid_ = function() { | |
| 266 var activeElement = document.activeElement; | |
| 267 this.focusGrid_.destroy(); | |
| 268 | |
| 269 var keys = Object.keys(this.downloads_); | |
| 270 for (var i = 0; i < keys.length; ++i) { | |
| 271 var download = this.downloads_[keys[i]]; | |
| 272 DownloadFocusRow.decorate(download.node, download, this.node_); | |
| 273 } | |
| 274 | |
| 275 // The ordering of the keys is not guaranteed, and downloads should be added | |
| 276 // to the FocusGrid in the order they will be in the UI. | |
| 277 var downloads = document.querySelectorAll('.download'); | |
| 278 for (var i = 0; i < downloads.length; ++i) { | |
| 279 var focusRow = downloads[i]; | |
| 280 this.focusGrid_.addRow(focusRow); | |
| 281 | |
| 282 // Focus the equivalent element in the focusRow because the active element | |
| 283 // may no longer be visible. | |
| 284 if (focusRow.contains(activeElement)) | |
| 285 focusRow.getEquivalentElement(activeElement).focus(); | |
| 286 } | |
| 287 }; | |
| 288 | |
| 289 /** | |
| 290 * Returns the number of downloads in the model. Used by tests. | |
| 291 * @return {number} Returns the number of downloads shown on the page. | |
| 292 */ | |
| 293 Downloads.prototype.size = function() { | |
| 294 return Object.keys(this.downloads_).length; | |
| 295 }; | |
| 296 | |
| 297 /** | |
| 298 * Called whenever the downloads lists items have changed (either by being | |
| 299 * updated, added, or removed). | |
| 300 * @private | |
| 301 */ | |
| 302 Downloads.prototype.onDownloadListChanged_ = function() { | |
| 303 // Update the date visibility in our nodes so that no date is repeated. | |
| 304 var dateContainers = document.getElementsByClassName('date-container'); | |
| 305 var displayed = {}; | |
| 306 for (var i = 0, container; container = dateContainers[i]; i++) { | |
| 307 var dateString = container.getElementsByClassName('date')[0].innerHTML; | |
| 308 if (displayed[dateString]) { | |
| 309 container.style.display = 'none'; | |
| 310 } else { | |
| 311 displayed[dateString] = true; | |
| 312 container.style.display = 'block'; | |
| 313 } | |
| 314 } | |
| 315 | |
| 316 this.updateResults(); | |
| 317 }; | |
| 318 | |
| 319 /** | |
| 320 * Remove a download. | |
| 321 * @param {string} id The id of the download to remove. | |
| 322 */ | |
| 323 Downloads.prototype.remove = function(id) { | |
| 324 this.node_.removeChild(this.downloads_[id].node); | |
| 325 delete this.downloads_[id]; | |
| 326 this.onDownloadListChanged_(); | |
| 327 }; | |
| 328 | |
| 329 /** Clear all downloads and reset us back to a null state. */ | |
| 330 Downloads.prototype.clear = function() { | |
| 331 for (var id in this.downloads_) { | |
| 332 this.downloads_[id].clear(); | |
| 333 this.remove(id); | |
| 334 } | |
| 335 }; | |
| 336 | |
| 337 /** | |
| 338 * Schedule icon load. | |
| 339 * @param {HTMLImageElement} elem Image element that should contain the icon. | |
| 340 * @param {string} iconURL URL to the icon. | |
| 341 */ | |
| 342 Downloads.prototype.scheduleIconLoad = function(elem, iconURL) { | |
| 343 var self = this; | |
| 344 | |
| 345 // Sends request to the next icon in the queue and schedules | |
| 346 // call to itself when the icon is loaded. | |
| 347 function loadNext() { | |
| 348 self.isIconLoading_ = true; | |
| 349 while (self.iconLoadQueue_.length > 0) { | |
| 350 var request = self.iconLoadQueue_.shift(); | |
| 351 var oldSrc = request.element.src; | |
| 352 request.element.onabort = request.element.onerror = | |
| 353 request.element.onload = loadNext; | |
| 354 request.element.src = request.url; | |
| 355 if (oldSrc != request.element.src) | |
| 356 return; | |
| 357 } | |
| 358 self.isIconLoading_ = false; | |
| 359 } | |
| 360 | |
| 361 // Create new request | |
| 362 var loadRequest = {element: elem, url: iconURL}; | |
| 363 this.iconLoadQueue_.push(loadRequest); | |
| 364 | |
| 365 // Start loading if none scheduled yet | |
| 366 if (!this.isIconLoading_) | |
| 367 loadNext(); | |
| 368 }; | |
| 369 | |
| 370 /** | |
| 371 * Returns whether the displayed list needs to be updated or not. | |
| 372 * @param {Array} downloads Array of download nodes. | |
| 373 * @return {boolean} Returns true if the displayed list is to be updated. | |
| 374 */ | |
| 375 Downloads.prototype.isUpdateNeeded = function(downloads) { | |
| 376 var size = 0; | |
| 377 for (var i in this.downloads_) | |
| 378 size++; | |
| 379 if (size != downloads.length) | |
| 380 return true; | |
| 381 // Since there are the same number of items in the incoming list as | |
| 382 // |this.downloads_|, there won't be any removed downloads without some | |
| 383 // downloads having been inserted. So check only for new downloads in | |
| 384 // deciding whether to update. | |
| 385 for (var i = 0; i < downloads.length; i++) { | |
| 386 if (!this.downloads_[downloads[i].id]) | |
| 387 return true; | |
| 388 } | |
| 389 return false; | |
| 390 }; | |
| 391 | |
| 392 /** | |
| 393 * @param {Event} e | |
| 394 * @private | |
| 395 */ | |
| 396 Downloads.prototype.onCanExecute_ = function(e) { | |
| 397 e = /** @type {cr.ui.CanExecuteEvent} */(e); | |
| 398 e.canExecute = document.activeElement != $('term'); | |
| 399 }; | |
| 400 | |
| 401 /** | |
| 402 * @param {Event} e | |
| 403 * @private | |
| 404 */ | |
| 405 Downloads.prototype.onCommand_ = function(e) { | |
| 406 if (e.command.id == 'undo-command') | |
| 407 chrome.send('undo'); | |
| 408 else if (e.command.id == 'clear-all-command') | |
| 409 clearAll(); | |
| 410 }; | |
| 411 | |
| 412 /////////////////////////////////////////////////////////////////////////////// | |
| 413 // Download | |
| 414 /** | |
| 415 * A download and the DOM representation for that download. | |
| 416 * @param {DownloadItem} download Info about the download. | |
| 417 * @constructor | |
| 418 */ | |
| 419 function Download(download) { | |
| 420 // Create DOM | |
| 421 this.node = createElementWithClassName( | |
| 422 'div', 'download' + (download.otr ? ' otr' : '')); | |
| 423 | |
| 424 // Dates | |
| 425 this.dateContainer_ = createElementWithClassName('div', 'date-container'); | |
| 426 this.node.appendChild(this.dateContainer_); | |
| 427 | |
| 428 this.nodeSince_ = createElementWithClassName('div', 'since'); | |
| 429 this.nodeDate_ = createElementWithClassName('div', 'date'); | |
| 430 this.dateContainer_.appendChild(this.nodeSince_); | |
| 431 this.dateContainer_.appendChild(this.nodeDate_); | |
| 432 | |
| 433 // Container for all 'safe download' UI. | |
| 434 this.safe_ = createElementWithClassName('div', 'safe'); | |
| 435 this.safe_.ondragstart = this.drag_.bind(this); | |
| 436 this.node.appendChild(this.safe_); | |
| 437 | |
| 438 if (download.state != Download.States.COMPLETE) { | |
| 439 this.nodeProgressBackground_ = | |
| 440 createElementWithClassName('div', 'progress background'); | |
| 441 this.safe_.appendChild(this.nodeProgressBackground_); | |
| 442 | |
| 443 this.nodeProgressForeground_ = | |
| 444 createElementWithClassName('canvas', 'progress'); | |
| 445 this.nodeProgressForeground_.width = Download.Progress.width; | |
| 446 this.nodeProgressForeground_.height = Download.Progress.height; | |
| 447 this.canvasProgress_ = this.nodeProgressForeground_.getContext('2d'); | |
| 448 | |
| 449 this.safe_.appendChild(this.nodeProgressForeground_); | |
| 450 } | |
| 451 | |
| 452 this.nodeImg_ = createElementWithClassName('img', 'icon'); | |
| 453 this.nodeImg_.alt = ''; | |
| 454 this.safe_.appendChild(this.nodeImg_); | |
| 455 | |
| 456 // FileLink is used for completed downloads, otherwise we show FileName. | |
| 457 this.nodeTitleArea_ = createElementWithClassName('div', 'title-area'); | |
| 458 this.safe_.appendChild(this.nodeTitleArea_); | |
| 459 | |
| 460 this.nodeFileLink_ = createActionLink(this.openFile_.bind(this)); | |
| 461 this.nodeFileLink_.className = 'name'; | |
| 462 this.nodeTitleArea_.appendChild(this.nodeFileLink_); | |
| 463 | |
| 464 this.nodeFileName_ = createElementWithClassName('span', 'name'); | |
| 465 this.nodeTitleArea_.appendChild(this.nodeFileName_); | |
| 466 | |
| 467 this.nodeStatus_ = createElementWithClassName('span', 'status'); | |
| 468 this.nodeTitleArea_.appendChild(this.nodeStatus_); | |
| 469 | |
| 470 var nodeURLDiv = createElementWithClassName('div', 'url-container'); | |
| 471 this.safe_.appendChild(nodeURLDiv); | |
| 472 | |
| 473 this.nodeURL_ = createElementWithClassName('a', 'src-url'); | |
| 474 this.nodeURL_.target = '_blank'; | |
| 475 nodeURLDiv.appendChild(this.nodeURL_); | |
| 476 | |
| 477 // Controls. | |
| 478 this.nodeControls_ = createElementWithClassName('div', 'controls'); | |
| 479 this.safe_.appendChild(this.nodeControls_); | |
| 480 | |
| 481 // We don't need 'show in folder' in chromium os. See download_ui.cc and | |
| 482 // http://code.google.com/p/chromium-os/issues/detail?id=916. | |
| 483 if (loadTimeData.valueExists('control_showinfolder')) { | |
| 484 this.controlShow_ = createActionLink(this.show_.bind(this), | |
| 485 loadTimeData.getString('control_showinfolder')); | |
| 486 this.nodeControls_.appendChild(this.controlShow_); | |
| 487 } else { | |
| 488 this.controlShow_ = null; | |
| 489 } | |
| 490 | |
| 491 this.controlRetry_ = document.createElement('a'); | |
| 492 this.controlRetry_.download = ''; | |
| 493 this.controlRetry_.textContent = loadTimeData.getString('control_retry'); | |
| 494 this.nodeControls_.appendChild(this.controlRetry_); | |
| 495 | |
| 496 // Pause/Resume are a toggle. | |
| 497 this.controlPause_ = createActionLink(this.pause_.bind(this), | |
| 498 loadTimeData.getString('control_pause')); | |
| 499 this.nodeControls_.appendChild(this.controlPause_); | |
| 500 | |
| 501 this.controlResume_ = createActionLink(this.resume_.bind(this), | |
| 502 loadTimeData.getString('control_resume')); | |
| 503 this.nodeControls_.appendChild(this.controlResume_); | |
| 504 | |
| 505 if (loadTimeData.getBoolean('allow_deleting_history')) { | |
| 506 this.controlRemove_ = createActionLink(this.remove_.bind(this), | |
| 507 loadTimeData.getString('control_removefromlist')); | |
| 508 this.controlRemove_.classList.add('control-remove-link'); | |
| 509 this.nodeControls_.appendChild(this.controlRemove_); | |
| 510 } | |
| 511 | |
| 512 this.controlCancel_ = createActionLink(this.cancel_.bind(this), | |
| 513 loadTimeData.getString('control_cancel')); | |
| 514 this.nodeControls_.appendChild(this.controlCancel_); | |
| 515 | |
| 516 this.controlByExtension_ = document.createElement('span'); | |
| 517 this.nodeControls_.appendChild(this.controlByExtension_); | |
| 518 | |
| 519 // Container for 'unsafe download' UI. | |
| 520 this.danger_ = createElementWithClassName('div', 'show-dangerous'); | |
| 521 this.node.appendChild(this.danger_); | |
| 522 | |
| 523 this.dangerNodeImg_ = createElementWithClassName('img', 'icon'); | |
| 524 this.dangerNodeImg_.alt = ''; | |
| 525 this.danger_.appendChild(this.dangerNodeImg_); | |
| 526 | |
| 527 this.dangerDesc_ = document.createElement('div'); | |
| 528 this.danger_.appendChild(this.dangerDesc_); | |
| 529 | |
| 530 // Buttons for the malicious case. | |
| 531 this.malwareNodeControls_ = createElementWithClassName('div', 'controls'); | |
| 532 this.malwareSave_ = createActionLink( | |
| 533 this.saveDangerous_.bind(this), | |
| 534 loadTimeData.getString('danger_restore')); | |
| 535 this.malwareNodeControls_.appendChild(this.malwareSave_); | |
| 536 this.malwareDiscard_ = createActionLink( | |
| 537 this.discardDangerous_.bind(this), | |
| 538 loadTimeData.getString('control_removefromlist')); | |
| 539 this.malwareNodeControls_.appendChild(this.malwareDiscard_); | |
| 540 this.danger_.appendChild(this.malwareNodeControls_); | |
| 541 | |
| 542 // Buttons for the dangerous but not malicious case. | |
| 543 this.dangerSave_ = createButton( | |
| 544 this.saveDangerous_.bind(this), | |
| 545 loadTimeData.getString('danger_save')); | |
| 546 this.danger_.appendChild(this.dangerSave_); | |
| 547 | |
| 548 this.dangerDiscard_ = createButton( | |
| 549 this.discardDangerous_.bind(this), | |
| 550 loadTimeData.getString('danger_discard')); | |
| 551 this.danger_.appendChild(this.dangerDiscard_); | |
| 552 | |
| 553 // Update member vars. | |
| 554 this.update(download); | |
| 555 } | |
| 556 | |
| 557 /** | |
| 558 * The states a download can be in. These correspond to states defined in | |
| 559 * DownloadsDOMHandler::CreateDownloadItemValue | |
| 560 * @enum {string} | |
| 561 */ | |
| 562 Download.States = { | |
| 563 IN_PROGRESS: 'IN_PROGRESS', | |
| 564 CANCELLED: 'CANCELLED', | |
| 565 COMPLETE: 'COMPLETE', | |
| 566 PAUSED: 'PAUSED', | |
| 567 DANGEROUS: 'DANGEROUS', | |
| 568 INTERRUPTED: 'INTERRUPTED', | |
| 569 }; | |
| 570 | |
| 571 /** | |
| 572 * Explains why a download is in DANGEROUS state. | |
| 573 * @enum {string} | |
| 574 */ | |
| 575 Download.DangerType = { | |
| 576 NOT_DANGEROUS: 'NOT_DANGEROUS', | |
| 577 DANGEROUS_FILE: 'DANGEROUS_FILE', | |
| 578 DANGEROUS_URL: 'DANGEROUS_URL', | |
| 579 DANGEROUS_CONTENT: 'DANGEROUS_CONTENT', | |
| 580 UNCOMMON_CONTENT: 'UNCOMMON_CONTENT', | |
| 581 DANGEROUS_HOST: 'DANGEROUS_HOST', | |
| 582 POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED', | |
| 583 }; | |
| 584 | |
| 585 /** | |
| 586 * @param {number} a Some float. | |
| 587 * @param {number} b Some float. | |
| 588 * @param {number=} opt_pct Percent of min(a,b). | |
| 589 * @return {boolean} true if a is within opt_pct percent of b. | |
| 590 */ | |
| 591 function floatEq(a, b, opt_pct) { | |
| 592 return Math.abs(a - b) < (Math.min(a, b) * (opt_pct || 1.0) / 100.0); | |
| 593 } | |
| 594 | |
| 595 /** Constants and "constants" for the progress meter. */ | |
| 596 Download.Progress = { | |
| 597 START_ANGLE: -0.5 * Math.PI, | |
| 598 SIDE: 48, | |
| 599 }; | |
| 600 | |
| 601 /***/ | |
| 602 Download.Progress.HALF = Download.Progress.SIDE / 2; | |
| 603 | |
| 604 function computeDownloadProgress() { | |
| 605 if (floatEq(Download.Progress.scale, window.devicePixelRatio)) { | |
| 606 // Zooming in or out multiple times then typing Ctrl+0 resets the zoom level | |
| 607 // directly to 1x, which fires the matchMedia event multiple times. | |
| 608 return; | |
| 609 } | |
| 610 Download.Progress.scale = window.devicePixelRatio; | |
| 611 Download.Progress.width = Download.Progress.SIDE * Download.Progress.scale; | |
| 612 Download.Progress.height = Download.Progress.SIDE * Download.Progress.scale; | |
| 613 Download.Progress.radius = Download.Progress.HALF * Download.Progress.scale; | |
| 614 Download.Progress.centerX = Download.Progress.HALF * Download.Progress.scale; | |
| 615 Download.Progress.centerY = Download.Progress.HALF * Download.Progress.scale; | |
| 616 } | |
| 617 computeDownloadProgress(); | |
| 618 | |
| 619 // Listens for when device-pixel-ratio changes between any zoom level. | |
| 620 [0.3, 0.4, 0.6, 0.7, 0.8, 0.95, 1.05, 1.2, 1.4, 1.6, 1.9, 2.2, 2.7, 3.5, 4.5 | |
| 621 ].forEach(function(scale) { | |
| 622 var media = '(-webkit-min-device-pixel-ratio:' + scale + ')'; | |
| 623 window.matchMedia(media).addListener(computeDownloadProgress); | |
| 624 }); | |
| 625 | |
| 626 /** | |
| 627 * Updates the download to reflect new data. | |
| 628 * @param {DownloadItem} download Updated info about this download. | |
| 629 */ | |
| 630 Download.prototype.update = function(download) { | |
| 631 this.id_ = download.id; | |
| 632 this.filePath_ = download.file_path; | |
| 633 this.fileUrl_ = download.file_url; | |
| 634 this.fileName_ = download.file_name; | |
| 635 this.url_ = download.url; | |
| 636 this.state_ = download.state; | |
| 637 this.fileExternallyRemoved_ = download.file_externally_removed; | |
| 638 this.dangerType_ = download.danger_type; | |
| 639 this.lastReasonDescription_ = download.last_reason_text; | |
| 640 this.byExtensionId_ = download.by_ext_id; | |
| 641 this.byExtensionName_ = download.by_ext_name; | |
| 642 | |
| 643 this.since_ = download.since_string; | |
| 644 this.date_ = download.date_string; | |
| 645 | |
| 646 // See DownloadItem::PercentComplete | |
| 647 this.percent_ = Math.max(download.percent, 0); | |
| 648 this.progressStatusText_ = download.progress_status_text; | |
| 649 this.received_ = download.received; | |
| 650 | |
| 651 if (this.state_ == Download.States.DANGEROUS) { | |
| 652 this.updateDangerousFile(); | |
| 653 } else { | |
| 654 downloads.scheduleIconLoad(this.nodeImg_, | |
| 655 'chrome://fileicon/' + | |
| 656 encodeURIComponent(this.filePath_) + | |
| 657 '?scale=' + window.devicePixelRatio + 'x'); | |
| 658 | |
| 659 if (this.state_ == Download.States.COMPLETE && | |
| 660 !this.fileExternallyRemoved_) { | |
| 661 this.nodeFileLink_.textContent = this.fileName_; | |
| 662 this.nodeFileLink_.href = this.fileUrl_; | |
| 663 this.nodeFileLink_.oncontextmenu = null; | |
| 664 } else if (this.nodeFileName_.textContent != this.fileName_) { | |
| 665 this.nodeFileName_.textContent = this.fileName_; | |
| 666 } | |
| 667 if (this.state_ == Download.States.INTERRUPTED) { | |
| 668 this.nodeFileName_.classList.add('interrupted'); | |
| 669 } else if (this.nodeFileName_.classList.contains('interrupted')) { | |
| 670 this.nodeFileName_.classList.remove('interrupted'); | |
| 671 } | |
| 672 | |
| 673 var completelyOnDisk = this.state_ == Download.States.COMPLETE && | |
| 674 !this.fileExternallyRemoved_; | |
| 675 this.nodeFileName_.hidden = completelyOnDisk; | |
| 676 this.nodeFileLink_.hidden = !completelyOnDisk; | |
| 677 | |
| 678 if (this.state_ == Download.States.IN_PROGRESS) { | |
| 679 this.nodeProgressForeground_.style.display = 'block'; | |
| 680 this.nodeProgressBackground_.style.display = 'block'; | |
| 681 this.nodeProgressForeground_.width = Download.Progress.width; | |
| 682 this.nodeProgressForeground_.height = Download.Progress.height; | |
| 683 | |
| 684 var foregroundImage = (window.devicePixelRatio < 2) ? | |
| 685 downloads.progressForeground1_ : downloads.progressForeground2_; | |
| 686 | |
| 687 // Draw a pie-slice for the progress. | |
| 688 this.canvasProgress_.globalCompositeOperation = 'copy'; | |
| 689 this.canvasProgress_.drawImage( | |
| 690 foregroundImage, | |
| 691 0, 0, // sx, sy | |
| 692 foregroundImage.width, | |
| 693 foregroundImage.height, | |
| 694 0, 0, // x, y | |
| 695 Download.Progress.width, Download.Progress.height); | |
| 696 this.canvasProgress_.globalCompositeOperation = 'destination-in'; | |
| 697 this.canvasProgress_.beginPath(); | |
| 698 this.canvasProgress_.moveTo(Download.Progress.centerX, | |
| 699 Download.Progress.centerY); | |
| 700 | |
| 701 // Draw an arc CW for both RTL and LTR. http://crbug.com/13215 | |
| 702 this.canvasProgress_.arc(Download.Progress.centerX, | |
| 703 Download.Progress.centerY, | |
| 704 Download.Progress.radius, | |
| 705 Download.Progress.START_ANGLE, | |
| 706 Download.Progress.START_ANGLE + Math.PI * 0.02 * | |
| 707 Number(this.percent_), | |
| 708 false); | |
| 709 | |
| 710 this.canvasProgress_.lineTo(Download.Progress.centerX, | |
| 711 Download.Progress.centerY); | |
| 712 this.canvasProgress_.fill(); | |
| 713 this.canvasProgress_.closePath(); | |
| 714 } else if (this.nodeProgressBackground_) { | |
| 715 this.nodeProgressForeground_.style.display = 'none'; | |
| 716 this.nodeProgressBackground_.style.display = 'none'; | |
| 717 } | |
| 718 | |
| 719 if (this.controlShow_) | |
| 720 this.controlShow_.hidden = !completelyOnDisk; | |
| 721 this.controlRetry_.hidden = !download.retry; | |
| 722 this.controlRetry_.href = this.url_; | |
| 723 this.controlPause_.hidden = this.state_ != Download.States.IN_PROGRESS; | |
| 724 this.controlResume_.hidden = !download.resume; | |
| 725 var showCancel = this.state_ == Download.States.IN_PROGRESS || | |
| 726 this.state_ == Download.States.PAUSED; | |
| 727 this.controlCancel_.hidden = !showCancel; | |
| 728 if (this.controlRemove_) | |
| 729 this.controlRemove_.hidden = showCancel; | |
| 730 | |
| 731 if (this.byExtensionId_ && this.byExtensionName_) { | |
| 732 // Format 'control_by_extension' with a link instead of plain text by | |
| 733 // splitting the formatted string into pieces. | |
| 734 var slug = 'XXXXX'; | |
| 735 var formatted = loadTimeData.getStringF('control_by_extension', slug); | |
| 736 var slugIndex = formatted.indexOf(slug); | |
| 737 this.controlByExtension_.textContent = formatted.substr(0, slugIndex); | |
| 738 this.controlByExtensionLink_ = document.createElement('a'); | |
| 739 this.controlByExtensionLink_.href = | |
| 740 'chrome://extensions#' + this.byExtensionId_; | |
| 741 this.controlByExtensionLink_.textContent = this.byExtensionName_; | |
| 742 this.controlByExtension_.appendChild(this.controlByExtensionLink_); | |
| 743 if (slugIndex < (formatted.length - slug.length)) | |
| 744 this.controlByExtension_.appendChild(document.createTextNode( | |
| 745 formatted.substr(slugIndex + 1))); | |
| 746 } | |
| 747 | |
| 748 this.nodeSince_.textContent = this.since_; | |
| 749 this.nodeDate_.textContent = this.date_; | |
| 750 // Don't unnecessarily update the url, as doing so will remove any | |
| 751 // text selection the user has started (http://crbug.com/44982). | |
| 752 if (this.nodeURL_.textContent != this.url_) { | |
| 753 this.nodeURL_.textContent = this.url_; | |
| 754 this.nodeURL_.href = this.url_; | |
| 755 } | |
| 756 this.nodeStatus_.textContent = this.getStatusText_(); | |
| 757 | |
| 758 this.danger_.style.display = 'none'; | |
| 759 this.safe_.style.display = 'block'; | |
| 760 } | |
| 761 }; | |
| 762 | |
| 763 /** | |
| 764 * Decorates the icons, strings, and buttons for a download to reflect the | |
| 765 * danger level of a file. Dangerous & malicious files are treated differently. | |
| 766 */ | |
| 767 Download.prototype.updateDangerousFile = function() { | |
| 768 switch (this.dangerType_) { | |
| 769 case Download.DangerType.DANGEROUS_FILE: { | |
| 770 this.dangerDesc_.textContent = loadTimeData.getStringF( | |
| 771 'danger_file_desc', this.fileName_); | |
| 772 break; | |
| 773 } | |
| 774 case Download.DangerType.DANGEROUS_URL: { | |
| 775 this.dangerDesc_.textContent = loadTimeData.getString('danger_url_desc'); | |
| 776 break; | |
| 777 } | |
| 778 case Download.DangerType.DANGEROUS_CONTENT: // Fall through. | |
| 779 case Download.DangerType.DANGEROUS_HOST: { | |
| 780 this.dangerDesc_.textContent = loadTimeData.getStringF( | |
| 781 'danger_content_desc', this.fileName_); | |
| 782 break; | |
| 783 } | |
| 784 case Download.DangerType.UNCOMMON_CONTENT: { | |
| 785 this.dangerDesc_.textContent = loadTimeData.getStringF( | |
| 786 'danger_uncommon_desc', this.fileName_); | |
| 787 break; | |
| 788 } | |
| 789 case Download.DangerType.POTENTIALLY_UNWANTED: { | |
| 790 this.dangerDesc_.textContent = loadTimeData.getStringF( | |
| 791 'danger_settings_desc', this.fileName_); | |
| 792 break; | |
| 793 } | |
| 794 } | |
| 795 | |
| 796 if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) { | |
| 797 downloads.scheduleIconLoad( | |
| 798 this.dangerNodeImg_, | |
| 799 'chrome://theme/IDR_WARNING?scale=' + window.devicePixelRatio + 'x'); | |
| 800 } else { | |
| 801 downloads.scheduleIconLoad( | |
| 802 this.dangerNodeImg_, | |
| 803 'chrome://theme/IDR_SAFEBROWSING_WARNING?scale=' + | |
| 804 window.devicePixelRatio + 'x'); | |
| 805 this.dangerDesc_.className = 'malware-description'; | |
| 806 } | |
| 807 | |
| 808 if (this.dangerType_ == Download.DangerType.DANGEROUS_CONTENT || | |
| 809 this.dangerType_ == Download.DangerType.DANGEROUS_HOST || | |
| 810 this.dangerType_ == Download.DangerType.DANGEROUS_URL || | |
| 811 this.dangerType_ == Download.DangerType.POTENTIALLY_UNWANTED) { | |
| 812 this.malwareNodeControls_.style.display = 'block'; | |
| 813 this.dangerDiscard_.style.display = 'none'; | |
| 814 this.dangerSave_.style.display = 'none'; | |
| 815 } else { | |
| 816 this.malwareNodeControls_.style.display = 'none'; | |
| 817 this.dangerDiscard_.style.display = 'inline'; | |
| 818 this.dangerSave_.style.display = 'inline'; | |
| 819 } | |
| 820 | |
| 821 this.danger_.style.display = 'block'; | |
| 822 this.safe_.style.display = 'none'; | |
| 823 }; | |
| 824 | |
| 825 /** Removes applicable bits from the DOM in preparation for deletion. */ | |
| 826 Download.prototype.clear = function() { | |
| 827 this.safe_.ondragstart = null; | |
| 828 this.nodeFileLink_.onclick = null; | |
| 829 if (this.controlShow_) { | |
| 830 this.controlShow_.onclick = null; | |
| 831 } | |
| 832 this.controlCancel_.onclick = null; | |
| 833 this.controlPause_.onclick = null; | |
| 834 this.controlResume_.onclick = null; | |
| 835 this.dangerDiscard_.onclick = null; | |
| 836 this.dangerSave_.onclick = null; | |
| 837 this.malwareDiscard_.onclick = null; | |
| 838 this.malwareSave_.onclick = null; | |
| 839 | |
| 840 this.node.innerHTML = ''; | |
| 841 }; | |
| 842 | |
| 843 /** | |
| 844 * @private | |
| 845 * @return {string} User-visible status update text. | |
| 846 */ | |
| 847 Download.prototype.getStatusText_ = function() { | |
| 848 switch (this.state_) { | |
| 849 case Download.States.IN_PROGRESS: | |
| 850 return this.progressStatusText_; | |
| 851 case Download.States.CANCELLED: | |
| 852 return loadTimeData.getString('status_cancelled'); | |
| 853 case Download.States.PAUSED: | |
| 854 return loadTimeData.getString('status_paused'); | |
| 855 case Download.States.DANGEROUS: | |
| 856 // danger_url_desc is also used by DANGEROUS_CONTENT. | |
| 857 var desc = this.dangerType_ == Download.DangerType.DANGEROUS_FILE ? | |
| 858 'danger_file_desc' : 'danger_url_desc'; | |
| 859 return loadTimeData.getString(desc); | |
| 860 case Download.States.INTERRUPTED: | |
| 861 return this.lastReasonDescription_; | |
| 862 case Download.States.COMPLETE: | |
| 863 return this.fileExternallyRemoved_ ? | |
| 864 loadTimeData.getString('status_removed') : ''; | |
| 865 } | |
| 866 assertNotReached(); | |
| 867 return ''; | |
| 868 }; | |
| 869 | |
| 870 /** | |
| 871 * Tells the backend to initiate a drag, allowing users to drag | |
| 872 * files from the download page and have them appear as native file | |
| 873 * drags. | |
| 874 * @return {boolean} Returns false to prevent the default action. | |
| 875 * @private | |
| 876 */ | |
| 877 Download.prototype.drag_ = function() { | |
| 878 chrome.send('drag', [this.id_]); | |
| 879 return false; | |
| 880 }; | |
| 881 | |
| 882 /** | |
| 883 * Tells the backend to open this file. | |
| 884 * @return {boolean} Returns false to prevent the default action. | |
| 885 * @private | |
| 886 */ | |
| 887 Download.prototype.openFile_ = function() { | |
| 888 chrome.send('openFile', [this.id_]); | |
| 889 return false; | |
| 890 }; | |
| 891 | |
| 892 /** | |
| 893 * Tells the backend that the user chose to save a dangerous file. | |
| 894 * @return {boolean} Returns false to prevent the default action. | |
| 895 * @private | |
| 896 */ | |
| 897 Download.prototype.saveDangerous_ = function() { | |
| 898 chrome.send('saveDangerous', [this.id_]); | |
| 899 return false; | |
| 900 }; | |
| 901 | |
| 902 /** | |
| 903 * Tells the backend that the user chose to discard a dangerous file. | |
| 904 * @return {boolean} Returns false to prevent the default action. | |
| 905 * @private | |
| 906 */ | |
| 907 Download.prototype.discardDangerous_ = function() { | |
| 908 chrome.send('discardDangerous', [this.id_]); | |
| 909 downloads.remove(this.id_); | |
| 910 return false; | |
| 911 }; | |
| 912 | |
| 913 /** | |
| 914 * Tells the backend to show the file in explorer. | |
| 915 * @return {boolean} Returns false to prevent the default action. | |
| 916 * @private | |
| 917 */ | |
| 918 Download.prototype.show_ = function() { | |
| 919 chrome.send('show', [this.id_]); | |
| 920 return false; | |
| 921 }; | |
| 922 | |
| 923 /** | |
| 924 * Tells the backend to pause this download. | |
| 925 * @return {boolean} Returns false to prevent the default action. | |
| 926 * @private | |
| 927 */ | |
| 928 Download.prototype.pause_ = function() { | |
| 929 chrome.send('pause', [this.id_]); | |
| 930 return false; | |
| 931 }; | |
| 932 | |
| 933 /** | |
| 934 * Tells the backend to resume this download. | |
| 935 * @return {boolean} Returns false to prevent the default action. | |
| 936 * @private | |
| 937 */ | |
| 938 Download.prototype.resume_ = function() { | |
| 939 chrome.send('resume', [this.id_]); | |
| 940 return false; | |
| 941 }; | |
| 942 | |
| 943 /** | |
| 944 * Tells the backend to remove this download from history and download shelf. | |
| 945 * @return {boolean} Returns false to prevent the default action. | |
| 946 * @private | |
| 947 */ | |
| 948 Download.prototype.remove_ = function() { | |
| 949 assert(loadTimeData.getBoolean('allow_deleting_history')); | |
| 950 chrome.send('remove', [this.id_]); | |
| 951 return false; | |
| 952 }; | |
| 953 | |
| 954 /** | |
| 955 * Tells the backend to cancel this download. | |
| 956 * @return {boolean} Returns false to prevent the default action. | |
| 957 * @private | |
| 958 */ | |
| 959 Download.prototype.cancel_ = function() { | |
| 960 chrome.send('cancel', [this.id_]); | |
| 961 return false; | |
| 962 }; | |
| 963 | |
| 964 /////////////////////////////////////////////////////////////////////////////// | |
| 965 // Page: | |
| 966 var downloads, resultsTimeout; | |
| 967 | |
| 968 // TODO(benjhayden): Rename Downloads to DownloadManager, downloads to | |
| 969 // downloadManager or theDownloadManager or DownloadManager.get() to prevent | |
| 970 // confusing Downloads with Download. | |
| 971 | |
| 972 /** | |
| 973 * The FIFO array that stores updates of download files to be appeared | |
| 974 * on the download page. It is guaranteed that the updates in this array | |
| 975 * are reflected to the download page in a FIFO order. | |
| 976 */ | |
| 977 var fifoResults; | |
| 978 | |
| 979 function load() { | |
| 980 chrome.send('onPageLoaded'); | |
| 981 fifoResults = []; | |
| 982 downloads = new Downloads(); | |
| 983 $('term').focus(); | |
| 984 setSearch(''); | |
| 985 | |
| 986 $('clear-all').onclick = function() { | |
| 987 chrome.send('clearAll'); | |
| 988 }; | |
| 989 | |
| 990 $('open-downloads-folder').onclick = function() { | |
| 991 chrome.send('openDownloadsFolder'); | |
| 992 }; | |
| 993 | |
| 994 $('term').onsearch = function(e) { | |
| 995 setSearch($('term').value); | |
| 996 }; | |
| 997 } | |
| 998 | |
| 999 function setSearch(searchText) { | |
| 1000 fifoResults.length = 0; | |
| 1001 downloads.setSearchText(searchText); | |
| 1002 searchText = searchText.toString().match(/(?:[^\s"]+|"[^"]*")+/g); | |
| 1003 if (searchText) { | |
| 1004 searchText = searchText.map(function(term) { | |
| 1005 // strip quotes | |
| 1006 return (term.match(/\s/) && | |
| 1007 term[0].match(/["']/) && | |
| 1008 term[term.length - 1] == term[0]) ? | |
| 1009 term.substr(1, term.length - 2) : term; | |
| 1010 }); | |
| 1011 } else { | |
| 1012 searchText = []; | |
| 1013 } | |
| 1014 chrome.send('getDownloads', searchText); | |
| 1015 } | |
| 1016 | |
| 1017 function clearAll() { | |
| 1018 if (!loadTimeData.getBoolean('allow_deleting_history')) | |
| 1019 return; | |
| 1020 | |
| 1021 fifoResults.length = 0; | |
| 1022 downloads.clear(); | |
| 1023 downloads.setSearchText(''); | |
| 1024 chrome.send('clearAll'); | |
| 1025 } | |
| 1026 | |
| 1027 /////////////////////////////////////////////////////////////////////////////// | |
| 1028 // Chrome callbacks: | |
| 1029 /** | |
| 1030 * Our history system calls this function with results from searches or when | |
| 1031 * downloads are added or removed. | |
| 1032 * @param {Array<Object>} results List of updates. | |
| 1033 */ | |
| 1034 function downloadsList(results) { | |
| 1035 if (downloads && downloads.isUpdateNeeded(results)) { | |
| 1036 if (resultsTimeout) | |
| 1037 clearTimeout(resultsTimeout); | |
| 1038 fifoResults.length = 0; | |
| 1039 downloads.clear(); | |
| 1040 downloadUpdated(results); | |
| 1041 } | |
| 1042 downloads.updateResults(); | |
| 1043 downloads.updateSummary(); | |
| 1044 } | |
| 1045 | |
| 1046 /** | |
| 1047 * When a download is updated (progress, state change), this is called. | |
| 1048 * @param {Array<Object>} results List of updates for the download process. | |
| 1049 */ | |
| 1050 function downloadUpdated(results) { | |
| 1051 // Sometimes this can get called too early. | |
| 1052 if (!downloads) | |
| 1053 return; | |
| 1054 | |
| 1055 fifoResults = fifoResults.concat(results); | |
| 1056 tryDownloadUpdatedPeriodically(); | |
| 1057 } | |
| 1058 | |
| 1059 /** | |
| 1060 * Try to reflect as much updates as possible within 50ms. | |
| 1061 * This function is scheduled again and again until all updates are reflected. | |
| 1062 */ | |
| 1063 function tryDownloadUpdatedPeriodically() { | |
| 1064 var start = Date.now(); | |
| 1065 while (fifoResults.length) { | |
| 1066 var result = fifoResults.shift(); | |
| 1067 downloads.updated(result); | |
| 1068 // Do as much as we can in 50ms. | |
| 1069 if (Date.now() - start > 50) { | |
| 1070 clearTimeout(resultsTimeout); | |
| 1071 resultsTimeout = setTimeout(tryDownloadUpdatedPeriodically, 5); | |
| 1072 break; | |
| 1073 } | |
| 1074 } | |
| 1075 } | |
| 1076 | |
| 1077 // Add handlers to HTML elements. | |
| 1078 window.addEventListener('DOMContentLoaded', load); | |
| OLD | NEW |