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 /** | |
6 * Wrapper for chrome.send. | |
7 */ | |
8 function chromeSend(func, arg) { | |
9 if (arg == undefined) | |
10 arg = ''; | |
11 | |
12 // Convert to string. | |
13 if (typeof arg == 'number') | |
14 arg = '' + arg; | |
15 | |
16 chrome.send(func, [arg]); | |
17 }; | |
18 | |
19 /** | |
20 * Create a child element. | |
21 * | |
22 * @param {string} type The type - div, span, etc. | |
23 * @param {string} className The class name | |
24 * @param {HTMLElement} parent Parent to append this child to. | |
25 * @param {string} textContent optional text content of child. | |
26 * @param {function(*)} onclick onclick function of child. | |
27 */ | |
28 function createChild(type, className, parent, textContent, onclick) { | |
29 var elem = document.createElement(type); | |
30 elem.className = className; | |
31 if (textContent !== undefined) | |
32 elem.textContent = textContent; | |
33 elem.onclick = onclick; | |
34 parent.appendChild(elem); | |
35 return elem; | |
36 }; | |
37 | |
38 var localStrings; | |
39 var downloadRowList; | |
40 | |
41 function init() { | |
42 localStrings = new LocalStrings(); | |
43 initTestHarness(); | |
44 | |
45 window.onkeydown = function(e) { | |
46 if (e.keyCode == 27) // Escape. | |
47 menu.clear(); | |
48 e.preventDefault(); // Suppress browser shortcuts. | |
49 }; | |
50 | |
51 document.body.addEventListener("blur", menu.clear); | |
52 document.body.addEventListener("click", menu.clear); | |
53 document.body.addEventListener("contextmenu", function (e) { | |
54 e.preventDefault(); }); | |
55 document.body.addEventListener("selectstart", function (e) { | |
56 e.preventDefault(); }); | |
57 | |
58 var sadt = $('showallfilestext'); | |
59 sadt.textContent = localStrings.getString('showallfiles'); | |
60 sadt.addEventListener("click", showAllFiles); | |
61 | |
62 downloadRowList = new DownloadRowList(); | |
63 chromeSend('getDownloads'); | |
64 } | |
65 | |
66 /** | |
67 * Testing. Allow this page to be loaded in a browser. | |
68 * Create stubs for localStrings and chrome.send. | |
69 */ | |
70 function initTestHarness() { | |
71 if (location.protocol != 'file:') | |
72 return; | |
73 | |
74 // Enable right click for dom inspector. | |
75 document.body.oncontextmenu = ''; | |
76 | |
77 // Fix localStrings. | |
78 localStrings = { | |
79 getString: function(name) { | |
80 if (name == 'showallfiles') | |
81 return 'Show all files'; | |
82 if (name == 'dangerousextension') | |
83 return 'Extensions, apps, and themes can harm your computer.' + | |
84 ' Are you sure you want to continue?' | |
85 if (name == 'continue') | |
86 return 'Continue'; | |
87 if (name == 'discard') | |
88 return 'Discard'; | |
89 return name; | |
90 }, | |
91 getStringF: function(name, path) { | |
92 return path + ' - Unknown file type.'; | |
93 }, | |
94 }; | |
95 | |
96 // Log chrome.send calls. | |
97 chrome.send = function(name, ary) { | |
98 console.log('chrome.send ' + name + ' ' + ary); | |
99 if (name == 'getDownloads' || | |
100 (name == 'openNewFullWindow' && | |
101 ary[0] == 'chrome://downloads')) | |
102 sendTestResults(); | |
103 }; | |
104 | |
105 // Fix resource images. | |
106 var cssRules = document.styleSheets[0].cssRules; | |
107 for (var i = 0; i < cssRules.length; i++) { | |
108 var cssRule = cssRules[i]; | |
109 if (cssRule.selectorText.match(/^div\.icon|^\.menuicon/)) { | |
110 cssRule.style.backgroundImage = | |
111 cssRule.style.backgroundImage.replace('chrome://resources', 'shared'); | |
112 } | |
113 } | |
114 } | |
115 | |
116 /** | |
117 * Create a results array with test data and call downloadsList. | |
118 */ | |
119 var testElement; | |
120 var testId = 0; | |
121 var testResults = []; | |
122 function sendTestResults() { | |
123 var testState = (testId % 3 == 0 ? 'IN_PROGRESS' : | |
124 (testId % 3 == 1 ? 'DANGEROUS' : 'COMPLETE')); | |
125 state1 = (testId % 3 == 0); | |
126 testResults.push({ | |
127 state: testState, | |
128 percent: (testId % 3 == 0 ? 90 : 100), | |
129 id: testId, | |
130 file_name: ' Test' + testId + '.pdf', | |
131 file_path: '/home/achuith/Downloads/Test' + testId + '.pdf', | |
132 progress_status_text : '107 MB/s - 108 MB of 672 MB, 5 secs left', | |
133 }); | |
134 testId++; | |
135 downloadsList(testResults); | |
136 } | |
137 | |
138 /** | |
139 * Current Menu. | |
140 */ | |
141 var menu = { | |
142 current_: null, | |
143 | |
144 /** | |
145 * Close the current menu. | |
146 */ | |
147 clear: function() { | |
148 var current = this.current_; | |
149 if (current) { | |
150 current.firstChild.style.display = 'none'; | |
151 current.style.opacity = ''; | |
152 this.current_ = null; | |
153 } | |
154 }, | |
155 | |
156 /** | |
157 * If it's a second click on an open menu, close the menu. | |
158 * Otherwise, close any other open menu and open the clicked menu. | |
159 */ | |
160 clicked: function(row) { | |
161 var menuicon = row.menuicon; | |
162 if (this.current_ === menuicon) { | |
163 this.clear(); | |
164 return; | |
165 } | |
166 this.clear(); | |
167 if (menuicon.firstChild.style.display != 'block') { | |
168 menuicon.firstChild.style.display = 'block'; | |
169 menuicon.style.opacity = '1'; | |
170 menuicon.scrollIntoView(); | |
171 this.current_ = menuicon; | |
172 } | |
173 window.event.stopPropagation(); | |
174 }, | |
175 }; | |
176 | |
177 function DiscardResult(result) { | |
178 return (result.state == 'CANCELLED' || | |
179 result.state == 'INTERRUPTED' || | |
180 result.state == 'REMOVING'); | |
181 }; | |
182 | |
183 /** | |
184 * C++ api calls. | |
185 */ | |
186 function downloadsList(results) { | |
187 downloadRowList.list(results); | |
188 } | |
189 | |
190 function downloadUpdated(result) { | |
191 downloadRowList.update(result); | |
192 } | |
193 | |
194 function showAllFiles() { | |
195 chromeSend('showAllFiles'); | |
196 } | |
197 | |
198 /** | |
199 * DownloadRow contains all the elements that go into a row of the downloads | |
200 * list. It represents a single DownloadItem. | |
201 * | |
202 * @param {DownloadRowList} list Global DownloadRowList. | |
203 * @param {Object} result JSON representation of DownloadItem. | |
204 * @constructor | |
205 */ | |
206 function DownloadRow(list, result) { | |
207 this.path = result.file_path; | |
208 this.name = result.file_name; | |
209 this.fileUrl = result.file_url; | |
210 this.list = list; | |
211 this.id = result.id; | |
212 | |
213 this.createRow_(list); | |
214 this.createMenu_(); | |
215 this.createRowButton_(); | |
216 this.setMenuHidden_(true); | |
217 } | |
218 | |
219 DownloadRow.prototype = { | |
220 /** | |
221 * Create the row html element and book-keeping for the row. | |
222 * @param {DownloadRowList} list global DownloadRowList instance. | |
223 * @private | |
224 */ | |
225 createRow_: function(list) { | |
226 var elem = document.createElement('li'); | |
227 elem.className = 'downloadrow'; | |
228 elem.id = this.path; | |
229 elem.row = this; | |
230 this.element = elem; | |
231 | |
232 list.append(this); | |
233 }, | |
234 | |
235 setDangerousIcon_: function(warning) { | |
236 var escapedPath = encodeURIComponent(this.path).replace(/'/g,'%27'); | |
237 this.icon.className = warning ? 'iconwarning' : 'icon'; | |
238 this.icon.style.background = warning ? '' : | |
239 'url(\'chrome://fileicon/' + escapedPath + | |
240 '?iconsize=small\') no-repeat'; | |
241 }, | |
242 | |
243 /** | |
244 * Create the row button for the left of the row. | |
245 * This contains the icon, filename and error elements. | |
246 * @private | |
247 */ | |
248 createRowButton_: function () { | |
249 this.rowbutton = createChild('div', 'rowbutton rowbg', this.element); | |
250 | |
251 // Icon. | |
252 this.icon = createChild('div', 'icon', this.rowbutton); | |
253 this.setDangerousIcon_(false); | |
254 | |
255 // Filename. | |
256 this.filename = createChild('span', 'title', this.rowbutton, this.name); | |
257 }, | |
258 | |
259 setMenuHidden_: function(hidden) { | |
260 this.menubutton.hidden = hidden; | |
261 if (hidden) { | |
262 this.rowbutton.style.width = '238px'; | |
263 } else { | |
264 this.rowbutton.style.width = ''; | |
265 } | |
266 }, | |
267 | |
268 /** | |
269 * Create the menu button on the right of the row. | |
270 * This contains the menuicon. The menuicon contains the menu, which | |
271 * contains items for Pause/Resume and Cancel. | |
272 * @private | |
273 */ | |
274 createMenu_: function() { | |
275 var self = this; | |
276 this.menubutton = createChild('div', 'menubutton rowbg', this.element, '', | |
277 function() { | |
278 menu.clicked(self); | |
279 }); | |
280 | |
281 this.menuicon = createChild('div', 'menuicon', this.menubutton); | |
282 | |
283 var menudiv = createChild('div', 'menu', this.menuicon); | |
284 | |
285 this.pause = createChild('div', 'menuitem', menudiv, | |
286 localStrings.getString('pause'), function() { | |
287 self.pauseToggleDownload_(); | |
288 }); | |
289 | |
290 this.cancel = createChild('div', 'menuitem', menudiv, | |
291 localStrings.getString('cancel'), function() { | |
292 self.cancelDownload_(); | |
293 }); | |
294 }, | |
295 | |
296 allowDownload_: function() { | |
297 chromeSend('allowDownload', this.id); | |
298 }, | |
299 | |
300 cancelDownload_: function() { | |
301 chromeSend('cancelDownload', this.id); | |
302 }, | |
303 | |
304 pauseToggleDownload_: function() { | |
305 this.pause.textContent = | |
306 (this.pause.textContent == localStrings.getString('pause')) ? | |
307 localStrings.getString('resume') : | |
308 localStrings.getString('pause'); | |
309 | |
310 chromeSend('pauseToggleDownload', this.id); | |
311 }, | |
312 | |
313 changeElemHeight_: function(elem, x) { | |
314 elem.style.height = elem.clientHeight + x + 'px'; | |
315 }, | |
316 | |
317 changeRowHeight_: function(x) { | |
318 this.list.rowsHeight += x; | |
319 this.changeElemHeight_(this.element, x); | |
320 // rowbutton has 5px padding. | |
321 this.changeElemHeight_(this.rowbutton, x - 5); | |
322 this.list.resize(); | |
323 }, | |
324 | |
325 DANGEROUS_HEIGHT: 60, | |
326 createDangerousPrompt_: function(dangerType) { | |
327 if (this.dangerous) | |
328 return; | |
329 | |
330 this.dangerous = createChild('div', 'dangerousprompt', this.rowbutton); | |
331 | |
332 // Handle dangerous files, extensions and dangerous urls. | |
333 var dangerText; | |
334 if (dangerType == 'DANGEROUS_URL') { | |
335 dangerText = localStrings.getString('dangerousurl'); | |
336 } else if (dangerType == 'DANGEROUS_CONTENT') { | |
337 dangerText = localStrings.getStringF('dangerouscontent', this.name); | |
338 } else if (dangerType == 'UNCOMMON_CONTENT') { | |
339 dangerText = localStrings.getStringF('uncommoncontent', this.name); | |
340 } else if (dangerType == 'DANGEROUS_FILE' && this.path.match(/\.crx$/)) { | |
341 dangerText = localStrings.getString('dangerousextension'); | |
342 } else { | |
343 dangerText = localStrings.getStringF('dangerousfile', this.name); | |
344 } | |
345 createChild('span', 'dangerousprompttext', this.dangerous, dangerText); | |
346 | |
347 var self = this; | |
348 createChild('span', 'confirm', this.dangerous, | |
349 localStrings.getString('discard'), | |
350 function() { | |
351 self.cancelDownload_(); | |
352 }); | |
353 createChild('span', 'confirm', this.dangerous, | |
354 localStrings.getString('continue'), | |
355 function() { | |
356 self.allowDownload_(); | |
357 }); | |
358 | |
359 this.changeRowHeight_(this.DANGEROUS_HEIGHT); | |
360 this.setDangerousIcon_(true); | |
361 }, | |
362 | |
363 removeDangerousPrompt_: function() { | |
364 if (!this.dangerous) | |
365 return; | |
366 | |
367 this.rowbutton.removeChild(this.dangerous); | |
368 this.dangerous = null; | |
369 | |
370 this.changeRowHeight_(-this.DANGEROUS_HEIGHT); | |
371 this.setDangerousIcon_(false); | |
372 }, | |
373 | |
374 PROGRESS_HEIGHT: 8, | |
375 createProgress_: function() { | |
376 if (this.progress) | |
377 return; | |
378 | |
379 this.progress = createChild('div', 'progress', this.rowbutton); | |
380 | |
381 this.setMenuHidden_(false); | |
382 this.changeRowHeight_(this.PROGRESS_HEIGHT); | |
383 }, | |
384 | |
385 removeProgress_: function() { | |
386 if (!this.progress) | |
387 return; | |
388 | |
389 this.rowbutton.removeChild(this.progress); | |
390 this.progress = null; | |
391 | |
392 this.changeRowHeight_(-this.PROGRESS_HEIGHT); | |
393 this.setMenuHidden_(true); | |
394 }, | |
395 | |
396 updatePause_: function(result) { | |
397 var pause = this.pause; | |
398 var pauseStr = localStrings.getString('pause'); | |
399 var resumeStr = localStrings.getString('resume'); | |
400 | |
401 if (pause && | |
402 result.state == 'PAUSED' && | |
403 pause.textContent != resumeStr) { | |
404 pause.textContent = resumeStr; | |
405 } else if (pause && | |
406 result.state == 'IN_PROGRESS' && | |
407 pause.textContent != pauseStr) { | |
408 pause.textContent = pauseStr; | |
409 } | |
410 }, | |
411 | |
412 progressStatusText_: function(progress) { | |
413 if (!progress) | |
414 return progress; | |
415 | |
416 /* m looks like this: | |
417 ["107 MB/s - 108 MB of 672 MB, 5 secs left", | |
418 "107 MB/s", "108", "MB", "672", "MB", "5 secs left"] | |
419 We want to return progress text like this: | |
420 "108 / 672 MB, 5 secs left" | |
421 or | |
422 "108 kB / 672 MB, 5 secs left" | |
423 */ | |
424 var m = progress.match( | |
425 /([^-]*) - ([0-9\.]*) ([a-zA-Z]*) of ([0-9\.]*) ([a-zA-Z]*), (.*)/); | |
426 if (!m || m.length != 7) | |
427 return progress; | |
428 | |
429 return m[2] + (m[3] == m[5] ? '' : ' ' + m[3]) + | |
430 ' / ' + m[4] + ' ' + m[5] + ', ' + m[6]; | |
431 }, | |
432 | |
433 updateProgress_: function(result) { | |
434 this.removeDangerousPrompt_(); | |
435 this.createProgress_(); | |
436 this.progress.textContent = | |
437 this.progressStatusText_(result.progress_status_text); | |
438 this.updatePause_(result); | |
439 }, | |
440 | |
441 /** | |
442 * Called when the item has finished downloading. Switch the menu | |
443 * and remove the progress bar. | |
444 * @private | |
445 */ | |
446 finishedDownloading_: function() { | |
447 // Make rowbutton clickable. | |
448 var self = this; | |
449 this.rowbutton.onclick = function() { | |
450 chromeSend('viewFile', self.path); | |
451 }; | |
452 | |
453 this.rowbutton.style.cursor = 'pointer'; | |
454 | |
455 // Make rowbutton draggable. | |
456 this.rowbutton.setAttribute('draggable', 'true'); | |
457 var self = this; | |
458 this.rowbutton.addEventListener('dragstart', function(e) { | |
459 e.dataTransfer.effectAllowed = 'copy'; | |
460 e.dataTransfer.setData('Text', self.path); | |
461 e.dataTransfer.setData('URL', self.fileUrl); | |
462 }, false); | |
463 | |
464 this.removeDangerousPrompt_(); | |
465 this.removeProgress_(); | |
466 }, | |
467 | |
468 /** | |
469 * One of the DownloadItem we are observing has updated. | |
470 * @param {Object} result JSON representation of DownloadItem. | |
471 */ | |
472 update: function(result) { | |
473 this.filename.textContent = result.file_name; | |
474 this.id = result.id; | |
475 | |
476 if (result.state != 'COMPLETE') { | |
477 this.rowbutton.onclick = ''; | |
478 this.rowbutton.style.cursor = ''; | |
479 } | |
480 | |
481 if (DiscardResult(result)) { | |
482 this.list.remove(this); | |
483 } else if (result.state == 'DANGEROUS') { | |
484 this.createDangerousPrompt_(result.danger_type); | |
485 } else if (result.percent < 100) { | |
486 this.updateProgress_(result); | |
487 } else if (result.state == 'COMPLETE') { | |
488 this.finishedDownloading_(); | |
489 } | |
490 }, | |
491 }; | |
492 | |
493 /** | |
494 * DownloadRowList is a container for DownloadRows. | |
495 */ | |
496 function DownloadRowList() { | |
497 this.element = createChild('ul', 'downloadlist', $('main')); | |
498 | |
499 document.title = localStrings.getString('downloadpath'). | |
500 split('/').pop(); | |
501 } | |
502 | |
503 DownloadRowList.prototype = { | |
504 | |
505 /** | |
506 * numRows is the current number of rows. | |
507 * rowsHeight is the sum of the heights of all rows. | |
508 * rowListHeight is the height of the container containing the rows. | |
509 * rows is the list of DownloadRows. | |
510 */ | |
511 numRows: 0, | |
512 rowsHeight: 0, | |
513 rowListHeight: 72, | |
514 rows: [], | |
515 | |
516 /** | |
517 * Resize the panel to accomodate all rows. | |
518 */ | |
519 resize: function() { | |
520 var diff = this.rowsHeight - this.rowListHeight; | |
521 if (diff != 0 && (this.rowListHeight + diff > 72)) { | |
522 window.resizeBy(0, diff); | |
523 this.rowListHeight += diff; | |
524 } | |
525 }, | |
526 | |
527 /** | |
528 * Remove a row from the list, as when a download is canceled, or | |
529 * the the number of rows has exceeded the max allowed. | |
530 * | |
531 * @param {DownloadRow} row Row to be removed. | |
532 * @private | |
533 */ | |
534 remove: function(row) { | |
535 this.rows.splice(this.rows.indexOf(row), 1); | |
536 | |
537 this.numRows--; | |
538 this.rowsHeight -= row.element.offsetHeight; | |
539 this.resize(); | |
540 | |
541 this.element.removeChild(row.element); | |
542 row.element.row = null; | |
543 }, | |
544 | |
545 removeList: function(rows) { | |
546 for (i = 0; i < rows.length; i++) { | |
547 this.remove(rows[i]); | |
548 } | |
549 }, | |
550 | |
551 updateList: function(results) { | |
552 for (var i = 0; i < results.length; i++) { | |
553 this.update(results[i]); | |
554 } | |
555 }, | |
556 | |
557 /** | |
558 * Append a new row to the list, removing the last row if we exceed the | |
559 * maximum allowed. | |
560 * @param {DownloadRow} row Row to be removed. | |
561 */ | |
562 append: function(row) { | |
563 this.rows.push(row); | |
564 | |
565 var elem = row.element; | |
566 var list = this.element; | |
567 if (list.firstChild) { | |
568 list.insertBefore(elem, list.firstChild); | |
569 } else { | |
570 list.appendChild(elem); | |
571 } | |
572 | |
573 this.rowsHeight += elem.offsetHeight; | |
574 | |
575 this.numRows++; | |
576 // We display no more than 5 elements. | |
577 if (this.numRows > 5) | |
578 this.remove(list.lastChild.row); | |
579 | |
580 this.resize(); | |
581 }, | |
582 | |
583 getRow: function(path) { | |
584 for (var i = 0; i < this.rows.length; i++) { | |
585 if (this.rows[i].path == path) | |
586 return this.rows[i]; | |
587 } | |
588 }, | |
589 | |
590 /** | |
591 * Returns the list of rows that are not in the results array. | |
592 * @param {Array} results Array of JSONified DownloadItems. | |
593 */ | |
594 findMissing: function(results) { | |
595 var removeList = []; | |
596 | |
597 for (var i = 0; i < this.rows.length; i++) { | |
598 var row = this.rows[i]; | |
599 var found = false; | |
600 for (var j = 0; j < results.length; j++) { | |
601 if (row.path == results[j].file_path) { | |
602 found = true; | |
603 break; | |
604 } | |
605 } | |
606 if (!found) | |
607 removeList.push(row); | |
608 } | |
609 return removeList; | |
610 }, | |
611 | |
612 /** | |
613 * Handle list callback with list of DownloadItems. | |
614 * @param {Array} results Array of JSONified DownloadItems. | |
615 */ | |
616 list: function(results) { | |
617 var rows = this.findMissing(results); | |
618 this.updateList(results); | |
619 this.removeList(rows); | |
620 }, | |
621 | |
622 /** | |
623 * Handle update of a DownloadItem we're observing. | |
624 * @param {Object} result JSON representation of DownloadItem. | |
625 */ | |
626 update: function(result) { | |
627 var row = this.getRow(result.file_path); | |
628 if (!row && !DiscardResult(result)) | |
629 row = new DownloadRow(this, result); | |
630 | |
631 row && row.update(result); | |
632 }, | |
633 }; | |
634 | |
635 document.addEventListener('DOMContentLoaded', init); | |
OLD | NEW |