Chromium Code Reviews| Index: chrome/browser/resources/chromeos/login/oobe_screen_user_image.js |
| diff --git a/chrome/browser/resources/chromeos/login/oobe_screen_user_image.js b/chrome/browser/resources/chromeos/login/oobe_screen_user_image.js |
| index 59eaa02509f69d7cdb7571420c42bc519afec37d..593854f1a62bf9c42b1bf97e2acbb52a8d79da82 100644 |
| --- a/chrome/browser/resources/chromeos/login/oobe_screen_user_image.js |
| +++ b/chrome/browser/resources/chromeos/login/oobe_screen_user_image.js |
| @@ -7,35 +7,60 @@ |
| */ |
| cr.define('oobe', function() { |
| - |
| var UserImagesGrid = options.UserImagesGrid; |
| var ButtonImages = UserImagesGrid.ButtonImages; |
| /** |
| * Array of button URLs used on this page. |
| * @type {Array.<string>} |
| + * @const |
| */ |
| - const ButtonImageUrls = [ |
| + var ButtonImageUrls = [ |
| ButtonImages.TAKE_PHOTO |
| ]; |
| /** |
| - * Creates a new oobe screen div. |
| + * Creates a new OOBE screen div. |
| * @constructor |
| * @extends {HTMLDivElement} |
| */ |
| var UserImageScreen = cr.ui.define('div'); |
| /** |
| + * Dimensions for camera capture. |
| + * @const |
| + */ |
| + var CAPTURE_SIZE = { |
| + height: 480, |
| + width: 480 |
| + }; |
| + |
| + /** |
| + * Interval between consecutive camera presence checks in msec while camera is |
| + * not present. |
| + * @const |
| + */ |
| + var CAMERA_CHECK_INTERVAL_MS = 1000; |
|
Nikita (slow)
2012/06/13 14:07:35
3000ms
Ivan Korotkov
2012/06/13 14:26:06
Done.
|
| + |
| + /** |
| + * Interval between consecutive camera liveness checks in msec. |
| + * @const |
| + */ |
| + var CAMERA_LIVENESS_CHECK_MS = 1000; |
| + |
| + /** |
| * Registers with Oobe. |
| */ |
| UserImageScreen.register = function() { |
| var screen = $('user-image'); |
| + var isWebRTC = document.documentElement.getAttribute('camera') == 'webrtc'; |
| + UserImageScreen.prototype = isWebRTC ? UserImageScreenWebRTCProto : |
| + UserImageScreenOldProto; |
| UserImageScreen.decorate(screen); |
| Oobe.getInstance().registerScreen(screen); |
| }; |
| - UserImageScreen.prototype = { |
| + var UserImageScreenOldProto = { |
| __proto__: HTMLDivElement.prototype, |
| /** |
| @@ -102,7 +127,7 @@ cr.define('oobe', function() { |
| okButton.id = 'ok-button'; |
| okButton.textContent = localStrings.getString('okButtonText'); |
| okButton.addEventListener('click', this.acceptImage_.bind(this)); |
| - return [ okButton ]; |
| + return [okButton]; |
| }, |
| /** |
| @@ -322,7 +347,455 @@ cr.define('oobe', function() { |
| /** |
| * Updates localized content of the screen that is not updated via template. |
| - * @public |
| + */ |
| + updateLocalizedContent: function() { |
| + this.updateProfileImageCaption_(); |
| + }, |
| + |
| + /** |
| + * Updates profile image caption. |
| + * @private |
| + */ |
| + updateProfileImageCaption_: function() { |
| + this.profileImageCaption = localStrings.getString( |
| + this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto'); |
| + } |
| + }; |
| + |
| + var UserImageScreenWebRTCProto = { |
| + __proto__: HTMLDivElement.prototype, |
| + |
| + /** |
| + * Currently selected user image index (take photo button is with zero |
| + * index). |
| + * @type {number} |
| + */ |
| + selectedUserImage_: -1, |
| + |
| + /** @inheritDoc */ |
| + decorate: function(element) { |
| + var imageGrid = $('user-image-grid'); |
| + UserImagesGrid.decorate(imageGrid); |
| + |
| + imageGrid.addEventListener('change', |
| + this.handleSelection_.bind(this)); |
| + imageGrid.addEventListener('activate', |
| + this.handleImageActivated_.bind(this)); |
| + imageGrid.addEventListener('dblclick', |
| + this.handleImageDblClick_.bind(this)); |
| + |
| + // Profile image data (if present). |
| + this.profileImage_ = imageGrid.addItem( |
| + ButtonImages.PROFILE_PICTURE, |
| + undefined, undefined, undefined, |
| + function(el) { // Custom decorator for Profile image element. |
| + var spinner = el.ownerDocument.createElement('div'); |
| + spinner.className = 'spinner'; |
| + var spinnerBg = el.ownerDocument.createElement('div'); |
| + spinnerBg.className = 'spinner-bg'; |
| + spinnerBg.appendChild(spinner); |
| + el.appendChild(spinnerBg); |
| + el.id = 'profile-image'; |
| + }); |
| + this.profileImage_.type = 'profile'; |
| + this.selectionType = 'default'; |
| + |
| + var video = $('user-image-stream'); |
| + video.addEventListener('canplay', this.handleVideoStarted_.bind(this)); |
| + video.addEventListener('timeupdate', this.handleVideoUpdate_.bind(this)); |
| + $('take-photo').addEventListener('click', |
| + this.handleTakePhoto_.bind(this)); |
| + $('discard-photo').addEventListener('click', |
| + this.handleDiscardPhoto_.bind(this)); |
| + this.cameraImage = null; |
| + // Perform an early check if camera is present, without starting capture. |
| + this.checkCameraPresence_(false, false); |
| + }, |
| + |
| + /** |
| + * Header text of the screen. |
| + * @type {string} |
| + */ |
| + get header() { |
| + return localStrings.getString('userImageScreenTitle'); |
| + }, |
| + |
| + /** |
| + * Buttons in oobe wizard's button strip. |
| + * @type {array} Array of Buttons. |
| + */ |
| + get buttons() { |
| + var okButton = this.ownerDocument.createElement('button'); |
| + okButton.id = 'ok-button'; |
| + okButton.textContent = localStrings.getString('okButtonText'); |
| + okButton.addEventListener('click', this.acceptImage_.bind(this)); |
| + return [okButton]; |
| + }, |
| + |
| + /** |
| + * The caption to use for the Profile image preview. |
| + * @type {string} |
| + */ |
| + get profileImageCaption() { |
| + return this.profileImageCaption_; |
| + }, |
| + set profileImageCaption(value) { |
| + this.profileImageCaption_ = value; |
| + this.updateCaption_(); |
| + }, |
| + |
| + /** |
| + * True if the Profile image is being loaded. |
| + * @type {boolean} |
| + */ |
| + get profileImageLoading() { |
| + return this.profileImageLoading_; |
| + }, |
| + set profileImageLoading(value) { |
| + this.profileImageLoading_ = value; |
| + $('user-image-screen-main').classList[ |
| + value ? 'add' : 'remove']('profile-image-loading'); |
| + this.updateProfileImageCaption_(); |
| + }, |
| + |
| + /** |
| + * True when camera is in live mode (i.e. no still photo selected). |
| + * @type {boolean} |
| + */ |
| + get cameraLive() { |
| + return this.cameraLive_; |
| + }, |
| + set cameraLive(value) { |
| + this.cameraLive_ = value; |
| + $('user-image-preview').classList[value ? 'add' : 'remove']('live'); |
| + }, |
| + |
| + /** |
| + * Type of the selected image (one of 'default', 'profile', 'camera'). |
| + * @type {string} |
| + */ |
| + get selectionType() { |
| + return this.selectionType_; |
| + }, |
| + set selectionType(value) { |
| + this.selectionType_ = value; |
| + var previewClassList = $('user-image-preview').classList; |
| + previewClassList[value == 'default' ? 'add' : 'remove']('default-image'); |
| + previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image'); |
| + previewClassList[value == 'camera' ? 'add' : 'remove']('camera'); |
| + this.updateCaption_(); |
| + }, |
| + |
| + /** |
| + * Handles image activation (by pressing Enter). |
| + * @private |
| + */ |
| + handleImageActivated_: function() { |
| + switch ($('user-image-grid').selectedItemUrl) { |
| + case ButtonImages.TAKE_PHOTO: |
| + this.handleTakePhoto_(); |
| + break; |
| + default: |
| + this.acceptImage_(); |
| + break; |
| + } |
| + }, |
| + |
| + /** |
| + * Handles photo capture from the live camera stream. |
| + * @private |
| + */ |
| + handleTakePhoto_: function() { |
| + var self = this; |
| + var photoURL = this.captureFrame_($('user-image-stream'), CAPTURE_SIZE); |
| + chrome.send('photoTaken', [photoURL]); |
| + // Wait until image is loaded before displaying it. |
| + var previewImg = new Image(); |
| + previewImg.addEventListener('load', function(e) { |
| + self.cameraImage = this.src; |
| + }); |
| + previewImg.src = photoURL; |
| + }, |
| + |
| + /** |
| + * Discard current photo and return to the live camera stream. |
| + * @private |
| + */ |
| + handleDiscardPhoto_: function() { |
| + this.cameraImage = null; |
| + }, |
| + |
| + /** |
| + * Capture a single still frame from a <video> element. |
| + * @param {HTMLVideoElement} video Video element to capture from. |
| + * @param {{width: number, height: number}} destSize Capture size. |
| + * @return {string} Captured frame as a data URL. |
| + * @private |
| + */ |
| + captureFrame_: function(video, destSize) { |
| + var canvas = document.createElement('canvas'); |
| + canvas.width = destSize.width; |
| + canvas.height = destSize.height; |
| + var ctx = canvas.getContext('2d'); |
| + var width = video.videoWidth; |
| + var height = video.videoHeight; |
| + if (width < destSize.width || height < destSize.height) { |
| + console.error('Video capture size too small: ' + |
| + width + 'x' + height + '!'); |
| + } |
| + var src = {}; |
| + if (width / destSize.width > height / destSize.height) { |
| + // Full height, crop left/right. |
| + src.height = height; |
| + src.width = height * destSize.width / destSize.height; |
| + } else { |
| + // Full width, crop top/bottom. |
| + src.width = width; |
| + src.height = width * destSize.height / destSize.width; |
| + } |
| + src.x = (width - src.width) / 2; |
| + src.y = (height - src.height) / 2; |
| + ctx.drawImage(video, src.x, src.y, src.width, src.height, |
| + 0, 0, destSize.width, destSize.height); |
| + return canvas.toDataURL('image/png'); |
| + }, |
| + |
| + /** |
| + * Handles selection change. |
| + * @private |
| + */ |
| + handleSelection_: function() { |
| + var selectedItem = $('user-image-grid').selectedItem; |
| + if (selectedItem === null) |
| + return; |
| + |
| + // Update preview image URL. |
| + var url = selectedItem.url; |
| + $('user-image-preview-img').src = url; |
| + |
| + // Update current selection type. |
| + this.selectionType = selectedItem.type; |
| + |
| + // Show grey silhouette with the same border as stock images. |
| + if (/^chrome:\/\/theme\//.test(url)) |
| + $('user-image-preview').classList.add('default-image'); |
| + |
| + if (ButtonImageUrls.indexOf(url) == -1) { |
| + // Non-button image is selected. |
| + $('ok-button').disabled = false; |
| + chrome.send('selectImage', [url]); |
| + } else { |
| + $('ok-button').disabled = true; |
| + } |
| + }, |
| + |
| + /** |
| + * Handles double click on the image grid. |
| + * @param {Event} e Double click Event. |
| + */ |
| + handleImageDblClick_: function(e) { |
| + // If an image is double-clicked and not the grid itself, handle this |
| + // as 'OK' button button press. |
| + if (e.target.id != 'user-image-grid') |
| + this.acceptImage_(); |
| + }, |
| + |
| + /** |
| + * Event handler that is invoked just before the screen is shown. |
| + * @param {object} data Screen init payload. |
| + */ |
| + onBeforeShow: function(data) { |
| + Oobe.getInstance().headerHidden = true; |
| + $('user-image-grid').updateAndFocus(); |
| + chrome.send('onUserImageScreenShown'); |
| + // Now check again for camera presence and start capture. |
| + this.checkCameraPresence_(true, true); |
| + }, |
| + |
| + /** |
| + * Event handler that is invoked just before the screen is hidden. |
| + */ |
| + onBeforeHide: function() { |
| + $('user-image-stream').src = ''; |
| + }, |
| + |
| + /** |
| + * Accepts currently selected image, if possible. |
| + * @private |
| + */ |
| + acceptImage_: function() { |
| + var okButton = $('ok-button'); |
| + if (!okButton.disabled) { |
| + // This ensures that #ok-button won't be re-enabled again. |
| + $('user-image-grid').disabled = true; |
| + okButton.disabled = true; |
| + chrome.send('onUserImageAccepted'); |
| + } |
| + }, |
| + |
| + /** |
| + * @param {boolean} present Whether a camera is present or not. |
| + */ |
| + get cameraPresent() { |
| + return this.cameraPresent_; |
| + }, |
| + set cameraPresent(value) { |
| + this.cameraPresent_ = value; |
| + if (this.cameraLive) |
| + this.cameraImage = null; |
| + }, |
| + |
| + /** |
| + * Start camera presence check. |
| + * @param {boolean} autoplay Whether to start capture immediately. |
| + * @param {boolean} preselect Whether to select camera automatically. |
| + * @private |
| + */ |
| + checkCameraPresence_: function(autoplay, preselect) { |
| + $('user-image-preview').classList.remove('online'); |
| + navigator.webkitGetUserMedia( |
| + {video: true}, |
| + this.handleCameraAvailable_.bind(this, autoplay, preselect), |
| + // When ready to capture camera, poll regularly for camera presence. |
| + this.handleCameraAbsent_.bind(this, /* recheck= */ autoplay)); |
| + }, |
| + |
| + /** |
| + * Handles successful camera check. |
| + * @param {boolean} autoplay Whether to start capture immediately. |
| + * @param {boolean} preselect Whether to select camera automatically. |
| + * @param {MediaStream} stream Stream object as returned by getUserMedia. |
| + * @private |
| + */ |
| + handleCameraAvailable_: function(autoplay, preselect, stream) { |
| + if (autoplay) |
| + $('user-image-stream').src = window.webkitURL.createObjectURL(stream); |
| + this.cameraPresent = true; |
| + if (preselect) |
| + $('user-image-grid').selectedItem = this.cameraImage; |
| + }, |
| + |
| + /** |
| + * Handles camera check failure. |
| + * @param {boolean} recheck Whether to check for camera again. |
| + * @param {NavigatorUserMediaError=} err Error object. |
| + * @private |
| + */ |
| + handleCameraAbsent_: function(recheck, err) { |
| + this.cameraPresent = false; |
|
Nikita (slow)
2012/06/13 14:07:35
$('user-image-preview').classList.remove('online')
Ivan Korotkov
2012/06/13 14:26:06
Done.
|
| + // |preselect| is |false| in this case to not override user's selection. |
| + if (recheck) { |
| + setTimeout(this.checkCameraPresence_.bind(this, true, false), |
| + CAMERA_CHECK_INTERVAL_MS); |
| + } |
| + if (this.cameraLiveCheckTimer_) { |
| + clearInterval(this.cameraLiveCheckTimer_); |
| + this.cameraLiveCheckTimer_ = null; |
| + } |
| + }, |
| + |
| + /** |
| + * Handles successful camera capture start. |
| + * @private |
| + */ |
| + handleVideoStarted_: function() { |
| + $('user-image-preview').classList.add('online'); |
| + this.cameraLiveCheckTimer_ = setInterval(this.checkCameraLive_.bind(this), |
| + CAMERA_LIVENESS_CHECK_MS); |
| + }, |
| + |
| + /** |
| + * Handles camera stream update. Called regularly (at rate no greater then |
| + * 4/sec) while camera stream is live. |
| + * @private |
| + */ |
| + handleVideoUpdate_: function() { |
| + this.lastFrameTime_ = new Date().getTime(); |
| + }, |
| + |
| + /** |
| + * Checks if camera is still live by comparing the timestamp of the last |
| + * 'timeupdate' event with the current time. |
| + * @private |
| + */ |
| + checkCameraLive_: function() { |
| + if (new Date().getTime() - this.lastFrameTime_ > CAMERA_LIVENESS_CHECK_MS) |
| + this.handleCameraAbsent_(true, null); |
| + }, |
| + |
| + /** |
| + * Current image captured from camera as data URL. Setting to null will |
| + * return to the live camera stream. |
| + * @type {string=} |
| + */ |
| + get cameraImage() { |
| + return this.cameraImage_; |
| + }, |
| + set cameraImage(imageUrl) { |
| + this.cameraLive = !imageUrl; |
| + var imageGrid = $('user-image-grid'); |
| + if (this.cameraPresent && !imageUrl) { |
| + imageUrl = ButtonImages.TAKE_PHOTO; |
| + } |
| + if (imageUrl) { |
| + this.cameraImage_ = this.cameraImage_ ? |
| + imageGrid.updateItem(this.cameraImage_, imageUrl) : |
| + imageGrid.addItem(imageUrl, undefined, undefined, 0); |
| + this.cameraImage_.type = 'camera'; |
| + } else { |
| + imageGrid.removeItem(this.cameraImage_); |
| + this.cameraImage_ = null; |
| + } |
| + imageGrid.focus(); |
| + }, |
| + |
| + /** |
| + * Updates user profile image. |
| + * @param {?string} imageUrl Image encoded as data URL. If null, user has |
| + * the default profile image, which we don't want to show. |
| + * @private |
| + */ |
| + setProfileImage_: function(imageUrl) { |
| + this.profileImageLoading = false; |
| + if (imageUrl !== null) { |
| + this.profileImage_ = |
| + $('user-image-grid').updateItem(this.profileImage_, imageUrl); |
| + } |
| + }, |
| + |
| + /** |
| + * Appends received images to the list. |
| + * @param {Array.<string>} images An array of URLs to user images. |
| + * @private |
| + */ |
| + setUserImages_: function(images) { |
| + var imageGrid = $('user-image-grid'); |
| + for (var i = 0, url; url = images[i]; i++) |
| + imageGrid.addItem(url).type = 'default'; |
| + }, |
| + |
| + /** |
| + * Selects user image with the given URL. |
| + * @param {string} url URL of the image to select. |
| + * @private |
| + */ |
| + setSelectedImage_: function(url) { |
| + var imageGrid = $('user-image-grid'); |
| + imageGrid.selectedItemUrl = url; |
| + imageGrid.focus(); |
| + }, |
| + |
| + /** |
| + * Updates the image preview caption. |
| + * @private |
| + */ |
| + updateCaption_: function() { |
| + $('user-image-preview-caption').textContent = |
| + (this.selectionType == 'profile') ? this.profileImageCaption : ''; |
| + }, |
| + |
| + /** |
| + * Updates localized content of the screen that is not updated via template. |
| */ |
| updateLocalizedContent: function() { |
| this.updateProfileImageCaption_(); |