OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 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 cr.define('downloads', function() { |
| 6 /** @const */ var Item = downloads.Item; |
| 7 |
| 8 /** |
| 9 * Creates and updates the DOM representation for a download. |
| 10 * @constructor |
| 11 */ |
| 12 function ItemView() { |
| 13 this.node = $('templates').querySelector('.download').cloneNode(true); |
| 14 |
| 15 this.safe_ = this.queryRequired_('.safe'); |
| 16 this.since_ = this.queryRequired_('.since'); |
| 17 this.dateContainer = this.queryRequired_('.date-container'); |
| 18 this.date_ = this.queryRequired_('.date'); |
| 19 this.save_ = this.queryRequired_('.save'); |
| 20 this.backgroundProgress_ = this.queryRequired_('.progress.background'); |
| 21 this.foregroundProgress_ = /** @type !HTMLCanvasElement */( |
| 22 this.queryRequired_('canvas.progress')); |
| 23 this.safeImg_ = /** @type !HTMLImageElement */( |
| 24 this.queryRequired_('.safe img')); |
| 25 this.fileName_ = this.queryRequired_('span.name'); |
| 26 this.fileLink = this.queryRequired_('[is="action-link"].name'); |
| 27 this.status_ = this.queryRequired_('.status'); |
| 28 this.srcUrl = this.queryRequired_('.src-url'); |
| 29 this.show = this.queryRequired_('.show'); |
| 30 this.retry = this.queryRequired_('.retry'); |
| 31 this.pause = this.queryRequired_('.pause'); |
| 32 this.resume = this.queryRequired_('.resume'); |
| 33 this.safeRemove = this.queryRequired_('.safe .remove'); |
| 34 this.cancel = this.queryRequired_('.cancel'); |
| 35 this.controlledBy = this.queryRequired_('.controlled-by'); |
| 36 |
| 37 this.dangerous_ = this.queryRequired_('.dangerous'); |
| 38 this.dangerImg_ = /** @type {!HTMLImageElement} */( |
| 39 this.queryRequired_('.dangerous img')); |
| 40 this.description_ = this.queryRequired_('.description'); |
| 41 this.malwareControls_ = this.queryRequired_('.dangerous .controls'); |
| 42 this.restore = this.queryRequired_('.restore'); |
| 43 this.dangerRemove = this.queryRequired_('.dangerous .remove'); |
| 44 this.save = this.queryRequired_('.save'); |
| 45 this.discard = this.queryRequired_('.discard'); |
| 46 |
| 47 // Event handlers (bound once on creation). |
| 48 this.safe_.ondragstart = this.onSafeDragstart_.bind(this); |
| 49 this.fileLink.onclick = this.onFileLinkClick_.bind(this); |
| 50 this.show.onclick = this.onShowClick_.bind(this); |
| 51 this.pause.onclick = this.onPauseClick_.bind(this); |
| 52 this.resume.onclick = this.onResumeClick_.bind(this); |
| 53 this.safeRemove.onclick = this.onSafeRemoveClick_.bind(this); |
| 54 this.cancel.onclick = this.onCancelClick_.bind(this); |
| 55 this.restore.onclick = this.onRestoreClick_.bind(this); |
| 56 this.save.onclick = this.onSaveClick_.bind(this); |
| 57 this.dangerRemove.onclick = this.onDangerRemoveClick_.bind(this); |
| 58 this.discard.onclick = this.onDiscardClick_.bind(this); |
| 59 } |
| 60 |
| 61 /** Progress meter constants. */ |
| 62 ItemView.Progress = { |
| 63 /** @const {number} */ |
| 64 START_ANGLE: -0.5 * Math.PI, |
| 65 /** @const {number} */ |
| 66 SIDE: 48, |
| 67 }; |
| 68 |
| 69 /** @const {number} */ |
| 70 ItemView.Progress.HALF = ItemView.Progress.SIDE / 2; |
| 71 |
| 72 ItemView.computeDownloadProgress = function() { |
| 73 /** |
| 74 * @param {number} a Some float. |
| 75 * @param {number} b Some float. |
| 76 * @param {number=} opt_pct Percent of min(a,b). |
| 77 * @return {boolean} true if a is within opt_pct percent of b. |
| 78 */ |
| 79 function floatEq(a, b, opt_pct) { |
| 80 return Math.abs(a - b) < (Math.min(a, b) * (opt_pct || 1.0) / 100.0); |
| 81 } |
| 82 |
| 83 if (floatEq(ItemView.Progress.scale, window.devicePixelRatio)) { |
| 84 // Zooming in or out multiple times then typing Ctrl+0 resets the zoom |
| 85 // level directly to 1x, which fires the matchMedia event multiple times. |
| 86 return; |
| 87 } |
| 88 var Progress = ItemView.Progress; |
| 89 Progress.scale = window.devicePixelRatio; |
| 90 Progress.width = Progress.SIDE * Progress.scale; |
| 91 Progress.height = Progress.SIDE * Progress.scale; |
| 92 Progress.radius = Progress.HALF * Progress.scale; |
| 93 Progress.centerX = Progress.HALF * Progress.scale; |
| 94 Progress.centerY = Progress.HALF * Progress.scale; |
| 95 }; |
| 96 ItemView.computeDownloadProgress(); |
| 97 |
| 98 // Listens for when device-pixel-ratio changes between any zoom level. |
| 99 [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]. |
| 100 forEach(function(scale) { |
| 101 var media = '(-webkit-min-device-pixel-ratio:' + scale + ')'; |
| 102 window.matchMedia(media).addListener(ItemView.computeDownloadProgress); |
| 103 }); |
| 104 |
| 105 /** |
| 106 * @return {!HTMLImageElement} The correct <img> to show when an item is |
| 107 * progressing in the foreground. |
| 108 */ |
| 109 ItemView.getForegroundProgressImage = function() { |
| 110 var x = window.devicePixelRatio >= 2 ? '2x' : '1x'; |
| 111 ItemView.foregroundImages_ = ItemView.foregroundImages_ || {}; |
| 112 if (!ItemView.foregroundImages_[x]) { |
| 113 ItemView.foregroundImages_[x] = new Image; |
| 114 var IMAGE_URL = 'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32'; |
| 115 ItemView.foregroundImages_[x].src = IMAGE_URL + '@' + x; |
| 116 } |
| 117 return ItemView.foregroundImages_[x]; |
| 118 }; |
| 119 |
| 120 /** @private {Array<{img: HTMLImageElement, url: string}>} */ |
| 121 ItemView.iconsToLoad_ = []; |
| 122 |
| 123 /** |
| 124 * Load the provided |url| into |img.src| after appending ?scale=. |
| 125 * @param {!HTMLImageElement} img An <img> to show the loaded image in. |
| 126 * @param {string} url A remote image URL to load. |
| 127 */ |
| 128 ItemView.loadScaledIcon = function(img, url) { |
| 129 var scale = '?scale=' + window.devicePixelRatio + 'x'; |
| 130 ItemView.iconsToLoad_.push({img: img, url: url + scale}); |
| 131 ItemView.loadNextIcon_(); |
| 132 }; |
| 133 |
| 134 /** @private */ |
| 135 ItemView.loadNextIcon_ = function() { |
| 136 if (ItemView.isIconLoading_) |
| 137 return; |
| 138 |
| 139 ItemView.isIconLoading_ = true; |
| 140 |
| 141 while (ItemView.iconsToLoad_.length) { |
| 142 var request = ItemView.iconsToLoad_.shift(); |
| 143 var img = request.img; |
| 144 |
| 145 if (img.src == request.url) |
| 146 continue; |
| 147 |
| 148 img.onabort = img.onerror = img.onload = function() { |
| 149 ItemView.isIconLoading_ = false; |
| 150 ItemView.loadNextIcon_(); |
| 151 }; |
| 152 |
| 153 img.src = request.url; |
| 154 return; |
| 155 } |
| 156 |
| 157 // If we reached here, there's no more work to do. |
| 158 ItemView.isIconLoading_ = false; |
| 159 }; |
| 160 |
| 161 ItemView.prototype = { |
| 162 /** @param {!downloads.Data} data */ |
| 163 update: function(data) { |
| 164 assert(!this.id_ || data.id == this.id_); |
| 165 this.id_ = data.id; // This is the only thing saved from |data|. |
| 166 |
| 167 this.node.classList.toggle('otr', data.otr); |
| 168 |
| 169 var dangerText = this.getDangerText_(data); |
| 170 this.dangerous_.hidden = !dangerText; |
| 171 this.safe_.hidden = !!dangerText; |
| 172 |
| 173 if (dangerText) { |
| 174 this.ensureTextIs_(this.description_, dangerText); |
| 175 |
| 176 var dangerousFile = data.danger_type == Item.DangerType.DANGEROUS_FILE; |
| 177 this.description_.classList.toggle('malware', !dangerousFile); |
| 178 |
| 179 var idr = dangerousFile ? 'IDR_WARNING' : 'IDR_SAFEBROWSING_WARNING'; |
| 180 ItemView.loadScaledIcon(this.dangerImg_, 'chrome://theme/' + idr); |
| 181 |
| 182 var showMalwareControls = |
| 183 data.danger_type == Item.DangerType.DANGEROUS_CONTENT || |
| 184 data.danger_type == Item.DangerType.DANGEROUS_HOST || |
| 185 data.danger_type == Item.DangerType.DANGEROUS_URL || |
| 186 data.danger_type == Item.DangerType.POTENTIALLY_UNWANTED; |
| 187 |
| 188 this.malwareControls_.hidden = !showMalwareControls; |
| 189 this.discard.hidden = showMalwareControls; |
| 190 this.save.hidden = showMalwareControls; |
| 191 } else { |
| 192 var path = encodeURIComponent(data.file_path); |
| 193 ItemView.loadScaledIcon(this.safeImg_, 'chrome://fileicon/' + path); |
| 194 |
| 195 /** @const */ var isInProgress = data.state == Item.States.IN_PROGRESS; |
| 196 this.node.classList.toggle('in-progress', isInProgress); |
| 197 |
| 198 /** @const */ var completelyOnDisk = |
| 199 data.state == Item.States.COMPLETE && !data.file_externally_removed; |
| 200 |
| 201 this.fileLink.href = data.url; |
| 202 this.ensureTextIs_(this.fileLink, data.file_name); |
| 203 this.fileLink.hidden = !completelyOnDisk; |
| 204 |
| 205 /** @const */ var isInterrupted = data.state == Item.States.INTERRUPTED; |
| 206 this.fileName_.classList.toggle('interrupted', isInterrupted); |
| 207 this.ensureTextIs_(this.fileName_, data.file_name); |
| 208 this.fileName_.hidden = completelyOnDisk; |
| 209 |
| 210 this.show.hidden = !completelyOnDisk; |
| 211 |
| 212 this.retry.href = data.url; |
| 213 this.retry.hidden = !data.retry; |
| 214 |
| 215 this.pause.hidden = !isInProgress; |
| 216 |
| 217 this.resume.hidden = !data.resume; |
| 218 |
| 219 /** @const */ var isPaused = data.state == Item.States.PAUSED; |
| 220 /** @const */ var showCancel = isPaused || isInProgress; |
| 221 this.cancel.hidden = !showCancel; |
| 222 |
| 223 this.safeRemove.hidden = showCancel || |
| 224 !loadTimeData.getBoolean('allow_deleting_history'); |
| 225 |
| 226 /** @const */ var controlledByExtension = data.by_ext_id && |
| 227 data.by_ext_name; |
| 228 this.controlledBy.hidden = !controlledByExtension; |
| 229 if (controlledByExtension) { |
| 230 var link = this.controlledBy.querySelector('a'); |
| 231 link.href = 'chrome://extensions#' + data.by_ext_id; |
| 232 link.textContent = data.by_ext_name; |
| 233 } |
| 234 |
| 235 this.ensureTextIs_(this.since_, data.since_string); |
| 236 this.ensureTextIs_(this.date_, data.date_string); |
| 237 this.ensureTextIs_(this.srcUrl, data.url); |
| 238 this.srcUrl.href = data.url; |
| 239 this.ensureTextIs_(this.status_, this.getStatusText_(data)); |
| 240 |
| 241 this.foregroundProgress_.hidden = !isInProgress; |
| 242 this.backgroundProgress_.hidden = !isInProgress; |
| 243 |
| 244 if (isInProgress) { |
| 245 this.foregroundProgress_.width = ItemView.Progress.width; |
| 246 this.foregroundProgress_.height = ItemView.Progress.height; |
| 247 |
| 248 if (!this.progressContext_) { |
| 249 /** @private */ |
| 250 this.progressContext_ = /** @type !CanvasRenderingContext2D */( |
| 251 this.foregroundProgress_.getContext('2d')); |
| 252 } |
| 253 |
| 254 var foregroundImage = ItemView.getForegroundProgressImage(); |
| 255 |
| 256 // Draw a pie-slice for the progress. |
| 257 this.progressContext_.globalCompositeOperation = 'copy'; |
| 258 this.progressContext_.drawImage( |
| 259 foregroundImage, |
| 260 0, 0, // sx, sy |
| 261 foregroundImage.width, |
| 262 foregroundImage.height, |
| 263 0, 0, // x, y |
| 264 ItemView.Progress.width, ItemView.Progress.height); |
| 265 |
| 266 this.progressContext_.globalCompositeOperation = 'destination-in'; |
| 267 this.progressContext_.beginPath(); |
| 268 this.progressContext_.moveTo(ItemView.Progress.centerX, |
| 269 ItemView.Progress.centerY); |
| 270 |
| 271 // Draw an arc CW for both RTL and LTR. http://crbug.com/13215 |
| 272 this.progressContext_.arc( |
| 273 ItemView.Progress.centerX, |
| 274 ItemView.Progress.centerY, |
| 275 ItemView.Progress.radius, |
| 276 ItemView.Progress.START_ANGLE, |
| 277 ItemView.Progress.START_ANGLE + Math.PI * 0.02 * data.percent, |
| 278 false); |
| 279 |
| 280 this.progressContext_.lineTo(ItemView.Progress.centerX, |
| 281 ItemView.Progress.centerY); |
| 282 this.progressContext_.fill(); |
| 283 this.progressContext_.closePath(); |
| 284 } |
| 285 } |
| 286 }, |
| 287 |
| 288 destroy: function() { |
| 289 if (this.node.parentNode) |
| 290 this.node.parentNode.removeChild(this.node); |
| 291 }, |
| 292 |
| 293 /** |
| 294 * @param {string} selector A CSS selector (e.g. '.class-name'). |
| 295 * @return {!Element} The element found by querying for |selector|. |
| 296 * @private |
| 297 */ |
| 298 queryRequired_: function(selector) { |
| 299 return assert(this.node.querySelector(selector)); |
| 300 }, |
| 301 |
| 302 /** |
| 303 * Overwrite |el|'s textContent if it differs from |text|. |
| 304 * @param {!Element} el |
| 305 * @param {string} text |
| 306 * @private |
| 307 */ |
| 308 ensureTextIs_: function(el, text) { |
| 309 if (el.textContent != text) |
| 310 el.textContent = text; |
| 311 }, |
| 312 |
| 313 /** |
| 314 * @param {!downloads.Data} data |
| 315 * @return {string} Text describing the danger of a download. Empty if not |
| 316 * dangerous. |
| 317 */ |
| 318 getDangerText_: function(data) { |
| 319 switch (data.danger_type) { |
| 320 case Item.DangerType.DANGEROUS_FILE: |
| 321 return loadTimeData.getStringF('danger_file_desc', data.file_name); |
| 322 case Item.DangerType.DANGEROUS_URL: |
| 323 return loadTimeData.getString('danger_url_desc'); |
| 324 case Item.DangerType.DANGEROUS_CONTENT: // Fall through. |
| 325 case Item.DangerType.DANGEROUS_HOST: |
| 326 return loadTimeData.getStringF('danger_content_desc', data.file_name); |
| 327 case Item.DangerType.UNCOMMON_CONTENT: |
| 328 return loadTimeData.getStringF('danger_uncommon_desc', |
| 329 data.file_name); |
| 330 case Item.DangerType.POTENTIALLY_UNWANTED: |
| 331 return loadTimeData.getStringF('danger_settings_desc', |
| 332 data.file_name); |
| 333 default: |
| 334 return ''; |
| 335 } |
| 336 }, |
| 337 |
| 338 /** |
| 339 * @param {!downloads.Data} data |
| 340 * @return {string} User-visible status update text. |
| 341 * @private |
| 342 */ |
| 343 getStatusText_: function(data) { |
| 344 switch (data.state) { |
| 345 case Item.States.IN_PROGRESS: |
| 346 return assert(data.progress_status_text); |
| 347 case Item.States.CANCELLED: |
| 348 return loadTimeData.getString('status_cancelled'); |
| 349 case Item.States.PAUSED: |
| 350 return loadTimeData.getString('status_paused'); |
| 351 case Item.States.DANGEROUS: |
| 352 break; // Intentionally hit assertNotReached(); at bottom. |
| 353 case Item.States.INTERRUPTED: |
| 354 return assert(data.last_reason_text); |
| 355 case Item.States.COMPLETE: |
| 356 return data.file_externally_removed ? |
| 357 loadTimeData.getString('status_removed') : ''; |
| 358 } |
| 359 assertNotReached(); |
| 360 return ''; |
| 361 }, |
| 362 |
| 363 /** |
| 364 * @private |
| 365 * @param {Event} e |
| 366 */ |
| 367 onSafeDragstart_: function(e) { |
| 368 e.preventDefault(); |
| 369 chrome.send('drag', [this.id_]); |
| 370 }, |
| 371 |
| 372 /** |
| 373 * @param {Event} e |
| 374 * @private |
| 375 */ |
| 376 onFileLinkClick_: function(e) { |
| 377 e.preventDefault(); |
| 378 chrome.send('openFile', [this.id_]); |
| 379 }, |
| 380 |
| 381 /** @private */ |
| 382 onShowClick_: function() { |
| 383 chrome.send('show', [this.id_]); |
| 384 }, |
| 385 |
| 386 /** @private */ |
| 387 onPauseClick_: function() { |
| 388 chrome.send('pause', [this.id_]); |
| 389 }, |
| 390 |
| 391 /** @private */ |
| 392 onResumeClick_: function() { |
| 393 chrome.send('resume', [this.id_]); |
| 394 }, |
| 395 |
| 396 /** @private */ |
| 397 onSafeRemoveClick_: function() { |
| 398 chrome.send('remove', [this.id_]); |
| 399 }, |
| 400 |
| 401 /** @private */ |
| 402 onCancelClick_: function() { |
| 403 chrome.send('cancel', [this.id_]); |
| 404 }, |
| 405 |
| 406 /** @private */ |
| 407 onRestoreClick_: function() { |
| 408 this.onSaveClick_(); |
| 409 }, |
| 410 |
| 411 /** @private */ |
| 412 onSaveClick_: function() { |
| 413 chrome.send('saveDangerous', [this.id_]); |
| 414 }, |
| 415 |
| 416 /** @private */ |
| 417 onDangerRemoveClick_: function() { |
| 418 this.onDiscardClick_(); |
| 419 }, |
| 420 |
| 421 /** @private */ |
| 422 onDiscardClick_: function() { |
| 423 chrome.send('discardDangerous', [this.id_]); |
| 424 }, |
| 425 }; |
| 426 |
| 427 return {ItemView: ItemView}; |
| 428 }); |
OLD | NEW |