| OLD | NEW |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview Oobe user image screen implementation. | 6 * @fileoverview Oobe user image screen implementation. |
| 7 */ | 7 */ |
| 8 | 8 |
| 9 cr.define('oobe', function() { | 9 cr.define('oobe', function() { |
| 10 | |
| 11 var UserImagesGrid = options.UserImagesGrid; | 10 var UserImagesGrid = options.UserImagesGrid; |
| 12 var ButtonImages = UserImagesGrid.ButtonImages; | 11 var ButtonImages = UserImagesGrid.ButtonImages; |
| 13 | 12 |
| 14 /** | 13 /** |
| 15 * Array of button URLs used on this page. | 14 * Array of button URLs used on this page. |
| 16 * @type {Array.<string>} | 15 * @type {Array.<string>} |
| 16 * @const |
| 17 */ | 17 */ |
| 18 const ButtonImageUrls = [ | 18 var ButtonImageUrls = [ |
| 19 ButtonImages.TAKE_PHOTO | 19 ButtonImages.TAKE_PHOTO |
| 20 ]; | 20 ]; |
| 21 | 21 |
| 22 /** | 22 /** |
| 23 * Creates a new oobe screen div. | 23 * Creates a new OOBE screen div. |
| 24 * @constructor | 24 * @constructor |
| 25 * @extends {HTMLDivElement} | 25 * @extends {HTMLDivElement} |
| 26 */ | 26 */ |
| 27 var UserImageScreen = cr.ui.define('div'); | 27 var UserImageScreen = cr.ui.define('div'); |
| 28 | 28 |
| 29 /** | 29 /** |
| 30 * Registers with Oobe. | 30 * Registers with Oobe. |
| 31 */ | 31 */ |
| 32 UserImageScreen.register = function() { | 32 UserImageScreen.register = function() { |
| 33 var screen = $('user-image'); | 33 var screen = $('user-image'); |
| 34 var isWebRTC = document.documentElement.getAttribute('camera') == 'webrtc'; |
| 35 console.log('WebRTC: ' + isWebRTC); |
| 36 UserImageScreen.prototype = isWebRTC ? UserImageScreenWebRTCProto : |
| 37 UserImageScreenOldProto; |
| 34 UserImageScreen.decorate(screen); | 38 UserImageScreen.decorate(screen); |
| 35 Oobe.getInstance().registerScreen(screen); | 39 Oobe.getInstance().registerScreen(screen); |
| 36 }; | 40 }; |
| 37 | 41 |
| 38 UserImageScreen.prototype = { | 42 var UserImageScreenOldProto = { |
| 39 __proto__: HTMLDivElement.prototype, | 43 __proto__: HTMLDivElement.prototype, |
| 40 | 44 |
| 41 /** | 45 /** |
| 42 * Currently selected user image index (take photo button is with zero | 46 * Currently selected user image index (take photo button is with zero |
| 43 * index). | 47 * index). |
| 44 * @type {number} | 48 * @type {number} |
| 45 */ | 49 */ |
| 46 selectedUserImage_: -1, | 50 selectedUserImage_: -1, |
| 47 | 51 |
| 48 /** @inheritDoc */ | 52 /** @inheritDoc */ |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 95 | 99 |
| 96 /** | 100 /** |
| 97 * Buttons in oobe wizard's button strip. | 101 * Buttons in oobe wizard's button strip. |
| 98 * @type {array} Array of Buttons. | 102 * @type {array} Array of Buttons. |
| 99 */ | 103 */ |
| 100 get buttons() { | 104 get buttons() { |
| 101 var okButton = this.ownerDocument.createElement('button'); | 105 var okButton = this.ownerDocument.createElement('button'); |
| 102 okButton.id = 'ok-button'; | 106 okButton.id = 'ok-button'; |
| 103 okButton.textContent = localStrings.getString('okButtonText'); | 107 okButton.textContent = localStrings.getString('okButtonText'); |
| 104 okButton.addEventListener('click', this.acceptImage_.bind(this)); | 108 okButton.addEventListener('click', this.acceptImage_.bind(this)); |
| 105 return [ okButton ]; | 109 return [okButton]; |
| 106 }, | 110 }, |
| 107 | 111 |
| 108 /** | 112 /** |
| 109 * The caption to use for the Profile image preview. | 113 * The caption to use for the Profile image preview. |
| 110 * @type {string} | 114 * @type {string} |
| 111 */ | 115 */ |
| 112 get profileImageCaption() { | 116 get profileImageCaption() { |
| 113 return this.profileImageCaption_; | 117 return this.profileImageCaption_; |
| 114 }, | 118 }, |
| 115 set profileImageCaption(value) { | 119 set profileImageCaption(value) { |
| (...skipping 199 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 315 * Updates the image preview caption. | 319 * Updates the image preview caption. |
| 316 * @private | 320 * @private |
| 317 */ | 321 */ |
| 318 updateCaption_: function() { | 322 updateCaption_: function() { |
| 319 $('user-image-preview-caption').textContent = | 323 $('user-image-preview-caption').textContent = |
| 320 this.profileImageSelected ? this.profileImageCaption : ''; | 324 this.profileImageSelected ? this.profileImageCaption : ''; |
| 321 }, | 325 }, |
| 322 | 326 |
| 323 /** | 327 /** |
| 324 * Updates localized content of the screen that is not updated via template. | 328 * Updates localized content of the screen that is not updated via template. |
| 325 * @public | |
| 326 */ | 329 */ |
| 327 updateLocalizedContent: function() { | 330 updateLocalizedContent: function() { |
| 328 this.updateProfileImageCaption_(); | 331 this.updateProfileImageCaption_(); |
| 332 }, |
| 333 |
| 334 /** |
| 335 * Updates profile image caption. |
| 336 * @private |
| 337 */ |
| 338 updateProfileImageCaption_: function() { |
| 339 this.profileImageCaption = localStrings.getString( |
| 340 this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto'); |
| 341 } |
| 342 }; |
| 343 |
| 344 var UserImageScreenWebRTCProto = { |
| 345 __proto__: HTMLDivElement.prototype, |
| 346 |
| 347 /** |
| 348 * Currently selected user image index (take photo button is with zero |
| 349 * index). |
| 350 * @type {number} |
| 351 */ |
| 352 selectedUserImage_: -1, |
| 353 |
| 354 /** @inheritDoc */ |
| 355 decorate: function(element) { |
| 356 var imageGrid = $('user-image-grid'); |
| 357 UserImagesGrid.decorate(imageGrid); |
| 358 |
| 359 imageGrid.addEventListener('change', |
| 360 this.handleSelection_.bind(this)); |
| 361 imageGrid.addEventListener('activate', |
| 362 this.handleImageActivated_.bind(this)); |
| 363 imageGrid.addEventListener('dblclick', |
| 364 this.handleImageDblClick_.bind(this)); |
| 365 |
| 366 // Profile image data (if present). |
| 367 this.profileImage_ = imageGrid.addItem( |
| 368 ButtonImages.PROFILE_PICTURE, |
| 369 undefined, undefined, undefined, |
| 370 function(el) { // Custom decorator for Profile image element. |
| 371 var spinner = el.ownerDocument.createElement('div'); |
| 372 spinner.className = 'spinner'; |
| 373 var spinnerBg = el.ownerDocument.createElement('div'); |
| 374 spinnerBg.className = 'spinner-bg'; |
| 375 spinnerBg.appendChild(spinner); |
| 376 el.appendChild(spinnerBg); |
| 377 el.id = 'profile-image'; |
| 378 }); |
| 379 this.profileImage_.type = 'profile'; |
| 380 this.selectionType = 'default'; |
| 381 |
| 382 var video = $('user-image-stream'); |
| 383 video.addEventListener('canplay', this.handleVideoStarted_.bind(this)); |
| 384 video.addEventListener('timeupdate', this.handleVideoUpdate_.bind(this)); |
| 385 $('take-photo').addEventListener('click', |
| 386 this.handleTakePhoto_.bind(this)); |
| 387 $('discard-photo').addEventListener('click', |
| 388 this.handleDiscardPhoto_.bind(this)); |
| 389 this.cameraImage = null; |
| 390 this.checkVideo_(); |
| 391 }, |
| 392 |
| 393 /** |
| 394 * Header text of the screen. |
| 395 * @type {string} |
| 396 */ |
| 397 get header() { |
| 398 return localStrings.getString('userImageScreenTitle'); |
| 399 }, |
| 400 |
| 401 /** |
| 402 * Buttons in oobe wizard's button strip. |
| 403 * @type {array} Array of Buttons. |
| 404 */ |
| 405 get buttons() { |
| 406 var okButton = this.ownerDocument.createElement('button'); |
| 407 okButton.id = 'ok-button'; |
| 408 okButton.textContent = localStrings.getString('okButtonText'); |
| 409 okButton.addEventListener('click', this.acceptImage_.bind(this)); |
| 410 return [okButton]; |
| 411 }, |
| 412 |
| 413 /** |
| 414 * The caption to use for the Profile image preview. |
| 415 * @type {string} |
| 416 */ |
| 417 get profileImageCaption() { |
| 418 return this.profileImageCaption_; |
| 419 }, |
| 420 set profileImageCaption(value) { |
| 421 this.profileImageCaption_ = value; |
| 422 this.updateCaption_(); |
| 423 }, |
| 424 |
| 425 /** |
| 426 * True if the Profile image is being loaded. |
| 427 * @type {boolean} |
| 428 */ |
| 429 get profileImageLoading() { |
| 430 return this.profileImageLoading_; |
| 431 }, |
| 432 set profileImageLoading(value) { |
| 433 this.profileImageLoading_ = value; |
| 434 $('user-image-screen-main').classList[ |
| 435 value ? 'add' : 'remove']('profile-image-loading'); |
| 436 this.updateProfileImageCaption_(); |
| 437 }, |
| 438 |
| 439 /** |
| 440 * True when camera is in live mode (i.e. no still photo selected). |
| 441 * @type {boolean} |
| 442 */ |
| 443 get cameraLive() { |
| 444 return this.cameraLive_; |
| 445 }, |
| 446 set cameraLive(value) { |
| 447 this.cameraLive_ = value; |
| 448 $('user-image-preview').classList[value ? 'add' : 'remove']('live'); |
| 449 }, |
| 450 |
| 451 /** |
| 452 * Type of the selected image (one of 'default', 'profile', 'camera'). |
| 453 * @type {string} |
| 454 */ |
| 455 get selectionType() { |
| 456 return this.selectionType_; |
| 457 }, |
| 458 set selectionType(value) { |
| 459 this.selectionType_ = value; |
| 460 var previewClassList = $('user-image-preview').classList; |
| 461 previewClassList[value == 'default' ? 'add' : 'remove']('default-image'); |
| 462 previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image'); |
| 463 previewClassList[value == 'camera' ? 'add' : 'remove']('camera'); |
| 464 this.updateCaption_(); |
| 465 }, |
| 466 |
| 467 /** |
| 468 * Handles image activation (by pressing Enter). |
| 469 * @private |
| 470 */ |
| 471 handleImageActivated_: function() { |
| 472 switch ($('user-image-grid').selectedItemUrl) { |
| 473 case ButtonImages.TAKE_PHOTO: |
| 474 this.handleTakePhoto_(); |
| 475 break; |
| 476 default: |
| 477 this.acceptImage_(); |
| 478 break; |
| 479 } |
| 480 }, |
| 481 |
| 482 /** |
| 483 * Handles photo capture from the live camera stream. |
| 484 * @private |
| 485 */ |
| 486 handleTakePhoto_: function() { |
| 487 var self = this; |
| 488 var photoURL = this.captureFrame_($('user-image-stream'), |
| 489 {width: 320, height: 320}); |
| 490 chrome.send('photoTaken', [photoURL]); |
| 491 // Wait until image is loaded before displaying it. |
| 492 var previewImg = new Image(); |
| 493 previewImg.addEventListener('load', function(e) { |
| 494 self.cameraImage = this.src; |
| 495 }); |
| 496 previewImg.src = photoURL; |
| 497 }, |
| 498 |
| 499 /** |
| 500 * Discard current photo and return to the live camera stream. |
| 501 * @private |
| 502 */ |
| 503 handleDiscardPhoto_: function() { |
| 504 this.cameraImage = null; |
| 505 }, |
| 506 |
| 507 /** |
| 508 * Capture a single still frame from a <video> element. |
| 509 * @param {HTMLVideoElement} video Video element to capture from. |
| 510 * @param {{width: number, height: number}} destSize Capture size. |
| 511 * @return {string} Captured frame as a data URL. |
| 512 * @private |
| 513 */ |
| 514 captureFrame_: function(video, destSize) { |
| 515 var canvas = document.createElement('canvas'); |
| 516 canvas.width = destSize.width; |
| 517 canvas.height = destSize.height; |
| 518 var ctx = canvas.getContext('2d'); |
| 519 var W = video.videoWidth, H = video.videoHeight; |
| 520 if (W < destSize.width || H < destSize.height) |
| 521 console.error('Video capture size too small: ' + W + 'x' + H + '!'); |
| 522 var src = {}; |
| 523 if (W / destSize.width > H / destSize.height) { |
| 524 // Full height, crop left/right. |
| 525 src.height = H; |
| 526 src.width = H * destSize.width / destSize.height; |
| 527 } else { |
| 528 // Full width, crop top/bottom. |
| 529 src.width = W; |
| 530 src.height = W * destSize.height / destSize.width; |
| 531 } |
| 532 src.x = (W - src.width) / 2; |
| 533 src.y = (H - src.height) / 2; |
| 534 ctx.drawImage(video, src.x, src.y, src.width, src.height, |
| 535 0, 0, destSize.width, destSize.height); |
| 536 return canvas.toDataURL('image/png'); |
| 537 }, |
| 538 |
| 539 /** |
| 540 * Handles selection change. |
| 541 * @private |
| 542 */ |
| 543 handleSelection_: function() { |
| 544 var selectedItem = $('user-image-grid').selectedItem; |
| 545 if (selectedItem === null) |
| 546 return; |
| 547 |
| 548 // Update preview image URL. |
| 549 var url = selectedItem.url; |
| 550 $('user-image-preview-img').src = url; |
| 551 |
| 552 // Update current selection type. |
| 553 this.selectionType = selectedItem.type; |
| 554 |
| 555 // Show grey silhouette with the same border as stock images. |
| 556 if (/^chrome:\/\/theme\//.test(url)) |
| 557 $('user-image-preview').classList.add('default-image'); |
| 558 |
| 559 if (ButtonImageUrls.indexOf(url) == -1) { |
| 560 // Non-button image is selected. |
| 561 $('ok-button').disabled = false; |
| 562 chrome.send('selectImage', [url]); |
| 563 } else { |
| 564 $('ok-button').disabled = true; |
| 565 } |
| 566 }, |
| 567 |
| 568 /** |
| 569 * Handles double click on the image grid. |
| 570 * @param {Event} e Double click Event. |
| 571 */ |
| 572 handleImageDblClick_: function(e) { |
| 573 // If an image is double-clicked and not the grid itself, handle this |
| 574 // as 'OK' button button press. |
| 575 if (e.target.id != 'user-image-grid') |
| 576 this.acceptImage_(); |
| 577 }, |
| 578 |
| 579 /** |
| 580 * Event handler that is invoked just before the screen is shown. |
| 581 * @param {object} data Screen init payload. |
| 582 */ |
| 583 onBeforeShow: function(data) { |
| 584 Oobe.getInstance().headerHidden = true; |
| 585 $('user-image-grid').updateAndFocus(); |
| 586 chrome.send('onUserImageScreenShown'); |
| 587 //this.checkVideo_(); |
| 588 }, |
| 589 |
| 590 /** |
| 591 * Accepts currently selected image, if possible. |
| 592 * @private |
| 593 */ |
| 594 acceptImage_: function() { |
| 595 var okButton = $('ok-button'); |
| 596 if (!okButton.disabled) { |
| 597 // This ensures that #ok-button won't be re-enabled again. |
| 598 $('user-image-grid').disabled = true; |
| 599 okButton.disabled = true; |
| 600 chrome.send('onUserImageAccepted'); |
| 601 } |
| 602 }, |
| 603 |
| 604 /** |
| 605 * @param {boolean} present Whether a camera is present or not. |
| 606 */ |
| 607 get cameraPresent() { |
| 608 return this.cameraPresent_; |
| 609 }, |
| 610 set cameraPresent(value) { |
| 611 this.cameraPresent_ = value; |
| 612 if (this.cameraLive) |
| 613 this.cameraImage = null; |
| 614 }, |
| 615 |
| 616 /** |
| 617 * Start camera presence check. |
| 618 * @private |
| 619 */ |
| 620 checkVideo_: function() { |
| 621 $('user-image-preview').classList.remove('online'); |
| 622 navigator.webkitGetUserMedia({video: true}, |
| 623 this.handleVideoAvailable_.bind(this), |
| 624 this.handleVideoFailed_.bind(this)); |
| 625 }, |
| 626 |
| 627 /** |
| 628 * Handles successful camera check. |
| 629 * @param {MediaStream} stream Stream object as returned by getUserMedia. |
| 630 * @private |
| 631 */ |
| 632 handleVideoAvailable_: function(stream) { |
| 633 $('user-image-stream').src = window.webkitURL.createObjectURL(stream); |
| 634 this.cameraPresent = true; |
| 635 }, |
| 636 |
| 637 /** |
| 638 * Handles camera check failure. |
| 639 * @param {NavigatorUserMediaError} err Error object. |
| 640 * @private |
| 641 */ |
| 642 handleVideoFailed_: function(err) { |
| 643 this.cameraPresent = false; |
| 644 }, |
| 645 |
| 646 /** |
| 647 * Handles successful camera capture start. |
| 648 * @private |
| 649 */ |
| 650 handleVideoStarted_: function() { |
| 651 $('user-image-preview').classList.add('online'); |
| 652 }, |
| 653 |
| 654 /** |
| 655 * Handles camera stream update. Called regularly (at rate no greater then |
| 656 * 4/sec) while camera stream is live. |
| 657 * @private |
| 658 */ |
| 659 handleVideoUpdate_: function() { |
| 660 if (!this.lastFrameTime_) { |
| 661 this.lastFrameTime_ = new Date().getTime(); |
| 662 return; |
| 663 } |
| 664 var newFrameTime = new Date().getTime(); |
| 665 this.lastFrameTime_ = newFrameTime; |
| 666 }, |
| 667 |
| 668 /** |
| 669 * Current image captured from camera as data URL. Setting to null will |
| 670 * return to the live camera stream. |
| 671 * @type {string=} |
| 672 */ |
| 673 get cameraImage() { |
| 674 return this.cameraImage_; |
| 675 }, |
| 676 set cameraImage(imageUrl) { |
| 677 this.cameraLive = !imageUrl; |
| 678 var imageGrid = $('user-image-grid'); |
| 679 if (this.cameraPresent && !imageUrl) { |
| 680 imageUrl = ButtonImages.TAKE_PHOTO; |
| 681 } |
| 682 if (imageUrl) { |
| 683 this.cameraImage_ = this.cameraImage_ ? |
| 684 imageGrid.updateItem(this.cameraImage_, imageUrl) : |
| 685 imageGrid.addItem(imageUrl, undefined, undefined, 0); |
| 686 this.cameraImage_.type = 'camera'; |
| 687 } else { |
| 688 imageGrid.removeItem(this.cameraImage_); |
| 689 this.cameraImage_ = null; |
| 690 } |
| 691 imageGrid.selectedItem = this.cameraImage_; |
| 692 imageGrid.focus(); |
| 693 }, |
| 694 |
| 695 /** |
| 696 * Updates user profile image. |
| 697 * @param {?string} imageUrl Image encoded as data URL. If null, user has |
| 698 * the default profile image, which we don't want to show. |
| 699 * @private |
| 700 */ |
| 701 setProfileImage_: function(imageUrl) { |
| 702 this.profileImageLoading = false; |
| 703 if (imageUrl !== null) { |
| 704 this.profileImage_ = |
| 705 $('user-image-grid').updateItem(this.profileImage_, imageUrl); |
| 706 } |
| 707 }, |
| 708 |
| 709 /** |
| 710 * Appends received images to the list. |
| 711 * @param {Array.<string>} images An array of URLs to user images. |
| 712 * @private |
| 713 */ |
| 714 setUserImages_: function(images) { |
| 715 var imageGrid = $('user-image-grid'); |
| 716 for (var i = 0, url; url = images[i]; i++) |
| 717 imageGrid.addItem(url).type = 'default'; |
| 718 }, |
| 719 |
| 720 /** |
| 721 * Selects user image with the given URL. |
| 722 * @param {string} url URL of the image to select. |
| 723 * @private |
| 724 */ |
| 725 setSelectedImage_: function(url) { |
| 726 var imageGrid = $('user-image-grid'); |
| 727 imageGrid.selectedItemUrl = url; |
| 728 imageGrid.focus(); |
| 729 }, |
| 730 |
| 731 /** |
| 732 * Updates the image preview caption. |
| 733 * @private |
| 734 */ |
| 735 updateCaption_: function() { |
| 736 $('user-image-preview-caption').textContent = |
| 737 (this.selectionType == 'profile') ? this.profileImageCaption : ''; |
| 738 }, |
| 739 |
| 740 /** |
| 741 * Updates localized content of the screen that is not updated via template. |
| 742 */ |
| 743 updateLocalizedContent: function() { |
| 744 this.updateProfileImageCaption_(); |
| 329 }, | 745 }, |
| 330 | 746 |
| 331 /** | 747 /** |
| 332 * Updates profile image caption. | 748 * Updates profile image caption. |
| 333 * @private | 749 * @private |
| 334 */ | 750 */ |
| 335 updateProfileImageCaption_: function() { | 751 updateProfileImageCaption_: function() { |
| 336 this.profileImageCaption = localStrings.getString( | 752 this.profileImageCaption = localStrings.getString( |
| 337 this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto'); | 753 this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto'); |
| 338 } | 754 } |
| 339 }; | 755 }; |
| 340 | 756 |
| 341 // Forward public APIs to private implementations. | 757 // Forward public APIs to private implementations. |
| 342 [ | 758 [ |
| 343 'setCameraPresent', | 759 'setCameraPresent', |
| 344 'setProfileImage', | 760 'setProfileImage', |
| 345 'setSelectedImage', | 761 'setSelectedImage', |
| 346 'setUserImages', | 762 'setUserImages', |
| 347 'setUserPhoto', | 763 'setUserPhoto', |
| 348 ].forEach(function(name) { | 764 ].forEach(function(name) { |
| 349 UserImageScreen[name] = function(value) { | 765 UserImageScreen[name] = function(value) { |
| 350 $('user-image')[name + '_'](value); | 766 $('user-image')[name + '_'](value); |
| 351 }; | 767 }; |
| 352 }); | 768 }); |
| 353 | 769 |
| 354 return { | 770 return { |
| 355 UserImageScreen: UserImageScreen | 771 UserImageScreen: UserImageScreen |
| 356 }; | 772 }; |
| 357 }); | 773 }); |
| OLD | NEW |