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 |