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