Commit b6a3e7121568a3d31b112fdc0a3a81edf2e95ff4

Authored by Vladyslav
Committed by Igor Kulikov
1 parent 75dec357

Feature/input widget webcamera (#1990)

* create widget and work to video

* style widget stream photo and create preview photo

* Add translate, save and style from widget
@@ -276,6 +276,22 @@ @@ -276,6 +276,22 @@
276 "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"readOnly\": {\n \"title\": \"Value is read only\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"readOnly\",\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n", 276 "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"readOnly\": {\n \"title\": \"Value is read only\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"readOnly\",\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n",
277 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" 277 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
278 } 278 }
  279 + },
  280 + {
  281 + "alias": "web_camera_input",
  282 + "name": "Web Camera Input",
  283 + "descriptor": {
  284 + "type": "latest",
  285 + "sizeX": 9.5,
  286 + "sizeY": 6.5,
  287 + "resources": [],
  288 + "templateHtml": "<tb-web-camera-widget ctx=\"ctx\">\n</tb-web-camera-widget>",
  289 + "templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
  290 + "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1\n }\n}\n\nself.onDestroy = function() {\n}\n",
  291 + "settingsSchema": "{}",
  292 + "dataKeySettingsSchema": "{}\n",
  293 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Web Camera Input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
  294 + }
279 } 295 }
280 ] 296 ]
281 } 297 }
@@ -25,6 +25,7 @@ import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy @@ -25,6 +25,7 @@ import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy
25 import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget'; 25 import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
26 import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator'; 26 import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator';
27 import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget'; 27 import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget';
  28 +import thingsboardWebCameraInputWidget from '../widget/lib/web-camera-input-widget';
28 29
29 import thingsboardRpcWidgets from '../widget/lib/rpc'; 30 import thingsboardRpcWidgets from '../widget/lib/rpc';
30 31
@@ -50,7 +51,8 @@ import thingsboardUtils from '../common/utils.service'; @@ -50,7 +51,8 @@ import thingsboardUtils from '../common/utils.service';
50 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, 51 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight,
51 thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, 52 thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget,
52 thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget, 53 thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
53 - thingsboardMultipleInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget]) 54 + thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes,
  55 + thingsboardUtils, TripAnimationWidget])
54 .factory('widgetService', WidgetService) 56 .factory('widgetService', WidgetService)
55 .name; 57 .name;
56 58
@@ -1693,10 +1693,15 @@ @@ -1693,10 +1693,15 @@
1693 "entity-timeseries-required": "Entity timeseries is required", 1693 "entity-timeseries-required": "Entity timeseries is required",
1694 "not-allowed-entity": "Selected entity cannot have shared attributes", 1694 "not-allowed-entity": "Selected entity cannot have shared attributes",
1695 "no-attribute-selected": "No attribute is selected", 1695 "no-attribute-selected": "No attribute is selected",
  1696 + "no-datakey-selected": "No datakey is selected",
1696 "no-entity-selected": "No entity selected", 1697 "no-entity-selected": "No entity selected",
  1698 + "no-image": "No image",
  1699 + "no-support-web-camera": "No supported web camera",
1697 "no-timeseries-selected": "No timeseries selected", 1700 "no-timeseries-selected": "No timeseries selected",
1698 "switch-attribute-value": "Switch entity attribute value", 1701 "switch-attribute-value": "Switch entity attribute value",
  1702 + "switch-camera": "Switch camera",
1699 "switch-timeseries-value": "Switch entity timeseries value", 1703 "switch-timeseries-value": "Switch entity timeseries value",
  1704 + "take-photo": "Take photo",
1700 "time": "Time", 1705 "time": "Time",
1701 "timeseries-not-allowed": "Timeseries parameter cannot be used in this widget", 1706 "timeseries-not-allowed": "Timeseries parameter cannot be used in this widget",
1702 "update-failed": "Update failed", 1707 "update-failed": "Update failed",
  1 +/*
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import './web-camera-input-widget.scss';
  17 +
  18 +/* eslint-disable import/no-unresolved, import/default */
  19 +import webCameraWidgetTemplate from './web-camera-input-widget.tpl.html';
  20 +/* eslint-enable import/no-unresolved, import/default */
  21 +
  22 +export default angular.module('thingsboard.widgets.webCameraWidget', [])
  23 + .directive('tbWebCameraWidget', webCameraWidget)
  24 + .name;
  25 +
  26 +/*@ngInject*/
  27 +function webCameraWidget() {
  28 + return {
  29 + restrict: "E",
  30 + scope: true,
  31 + bindToController: {
  32 + ctx: '='
  33 + },
  34 + controller: WebCameraWidgetController,
  35 + controllerAs: 'vm',
  36 + templateUrl: webCameraWidgetTemplate
  37 + };
  38 +}
  39 +
  40 +function WebCameraWidgetController($element, $scope, $window, types, utils, attributeService, Fullscreen) {
  41 + let vm = this;
  42 +
  43 + vm.videoInput = [];
  44 + vm.videoDevice = "";
  45 + vm.previewPhoto = "";
  46 + vm.isShowCamera = false;
  47 + vm.isPreviewPhoto = false;
  48 +
  49 + let streamDevice = null;
  50 + let indexWebCamera = 0;
  51 + let videoElement = null;
  52 + let canvas = null;
  53 + let photoCamera = null;
  54 + let dataKeyType = "";
  55 +
  56 + vm.getStream = getStream;
  57 + vm.createPhoto = createPhoto;
  58 + vm.takePhoto = takePhoto;
  59 + vm.switchWebCamera = switchWebCamera;
  60 + vm.cancelPhoto = cancelPhoto;
  61 + vm.closeCamera = closeCamera;
  62 + vm.savePhoto = savePhoto;
  63 +
  64 + vm.isEntityDetected = false;
  65 + vm.dataKeyDetected = false;
  66 + vm.isCameraSupport = false;
  67 + vm.isDeviceDetect = false;
  68 +
  69 + $scope.$watch('vm.ctx', function () {
  70 + if (vm.ctx && vm.ctx.datasources && vm.ctx.datasources.length) {
  71 + let datasource = vm.ctx.datasources[0];
  72 + if (datasource.type === types.datasourceType.entity) {
  73 + if (datasource.entityType && datasource.entityId) {
  74 + if (vm.ctx.settings.widgetTitle && vm.ctx.settings.widgetTitle.length) {
  75 + $scope.titleTemplate = utils.customTranslation(vm.ctx.settings.widgetTitle, vm.ctx.settings.widgetTitle);
  76 + } else {
  77 + $scope.titleTemplate = vm.ctx.widgetConfig.title;
  78 + }
  79 + vm.isEntityDetected = true;
  80 + }
  81 + }
  82 + if (datasource.dataKeys.length) {
  83 + $scope.currentKey = datasource.dataKeys[0].name;
  84 + dataKeyType = datasource.dataKeys[0].type;
  85 + vm.dataKeyDetected = true;
  86 + }
  87 + if (hasGetUserMedia()) {
  88 + vm.isCameraSupport = true;
  89 + getDevices().then(gotDevices).then(() => {
  90 + vm.isDeviceDetect = !!vm.videoInput.length;
  91 + });
  92 + }
  93 + }
  94 + });
  95 +
  96 + function hasGetUserMedia() {
  97 + return !!($window.navigator.mediaDevices && $window.navigator.mediaDevices.getUserMedia);
  98 + }
  99 +
  100 + function takePhoto(){
  101 + vm.isShowCamera = true;
  102 + videoElement = $element[0].querySelector('#videoStream');
  103 + photoCamera = $element[0].querySelector('#photoCamera');
  104 + canvas = $element[0].querySelector('canvas');
  105 + Fullscreen.enable(photoCamera);
  106 + getStream();
  107 + }
  108 +
  109 + function cancelPhoto() {
  110 + vm.isPreviewPhoto = false;
  111 + vm.previewPhoto = "";
  112 + }
  113 +
  114 + function switchWebCamera() {
  115 + indexWebCamera = (indexWebCamera+1)%vm.videoInput.length;
  116 + vm.videoDevice = vm.videoInput[indexWebCamera].deviceId;
  117 + getStream();
  118 + }
  119 +
  120 + function getDevices() {
  121 + return $window.navigator.mediaDevices.enumerateDevices();
  122 + }
  123 +
  124 + function gotDevices(deviceInfos) {
  125 + for (const deviceInfo of deviceInfos) {
  126 + let device = {
  127 + deviceId: deviceInfo.deviceId,
  128 + label: ""
  129 + };
  130 + if (deviceInfo.kind === 'videoinput') {
  131 + device.label = deviceInfo.label || `Camera ${vm.videoInput.length + 1}`;
  132 + vm.videoInput.push(device);
  133 + }
  134 + }
  135 + }
  136 +
  137 + function getStream() {
  138 + if (streamDevice !== null) {
  139 + streamDevice.getTracks().forEach(track => {
  140 + track.stop();
  141 + });
  142 + }
  143 + const constraints = {
  144 + video: {deviceId: vm.videoDevice !== "" ? {exact: vm.videoDevice} : undefined}
  145 + };
  146 + return $window.navigator.mediaDevices.getUserMedia(constraints).then(gotStream);
  147 + }
  148 +
  149 + function gotStream(stream) {
  150 + streamDevice = stream;
  151 + if(vm.videoDevice === ""){
  152 + indexWebCamera = vm.videoInput.findIndex(option => option.label === stream.getVideoTracks()[0].label);
  153 + indexWebCamera = indexWebCamera === -1 ? 0 : indexWebCamera;
  154 + vm.videoDevice = vm.videoInput[indexWebCamera].deviceId;
  155 + }
  156 + videoElement.srcObject = stream;
  157 + }
  158 +
  159 + function createPhoto() {
  160 + canvas.width = videoElement.videoWidth;
  161 + canvas.height = videoElement.videoHeight;
  162 + canvas.getContext('2d').drawImage(videoElement, 0, 0);
  163 + vm.previewPhoto = canvas.toDataURL('image/png');
  164 + vm.isPreviewPhoto = true;
  165 + }
  166 +
  167 + function closeCamera(){
  168 + Fullscreen.cancel(photoCamera);
  169 + vm.isShowCamera = false;
  170 + if (streamDevice !== null) {
  171 + streamDevice.getTracks().forEach(track => {
  172 + track.stop();
  173 + });
  174 + }
  175 + streamDevice = null;
  176 + videoElement.srcObject = null;
  177 + }
  178 +
  179 + function savePhoto(){
  180 + let promiseData = null;
  181 + let datasource = vm.ctx.datasources[0];
  182 + let saveData = [{
  183 + key: datasource.dataKeys[0].name,
  184 + value: vm.previewPhoto
  185 + }];
  186 + if(dataKeyType === types.dataKeyType.attribute){
  187 + promiseData = attributeService.saveEntityAttributes(datasource.entityType, datasource.entityId, types.attributesScope.server.value, saveData);
  188 + } else if(dataKeyType === types.dataKeyType.timeseries){
  189 + promiseData = attributeService.saveEntityTimeseries(datasource.entityType, datasource.entityId, "scope", saveData);
  190 + }
  191 + promiseData.then(()=>{
  192 + vm.isPreviewPhoto = false;
  193 + closeCamera();
  194 + })
  195 + }
  196 +}
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-web-camera {
  18 + height: 100%;
  19 +
  20 + &__last-photo{
  21 + width: 100%;
  22 + margin: 5px 0;
  23 + text-align: center;
  24 + border: solid 1px;
  25 +
  26 + &_text{
  27 + position: absolute;
  28 + top: 50%;
  29 + left: 50%;
  30 + margin-top: -.625em;
  31 + transform: translate(-50%, -50%);
  32 + }
  33 +
  34 + &_img{
  35 + width: 100%;
  36 + height: 100%;
  37 + object-fit: contain;
  38 + }
  39 + }
  40 +
  41 + .camera {
  42 + position: relative;
  43 + width: 100%;
  44 + height: 100%;
  45 + overflow: hidden;
  46 +
  47 + .camera-stream{
  48 + display: block;
  49 + width: 100%;
  50 + height: 100%;
  51 + object-fit: contain;
  52 + }
  53 +
  54 + .camera-controls {
  55 + position: absolute;
  56 + bottom: 0;
  57 + width: 100%;
  58 + padding: 0 5px 5px;
  59 + }
  60 + }
  61 +
  62 + .message-text{
  63 + font-size: 18px;
  64 + color: #a0a0a0;
  65 + text-align: center;
  66 + }
  67 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div layout="column" layout-align="center center" class="tb-web-camera">
  19 + <div ng-if="vm.isEntityDetected && vm.dataKeyDetected && vm.isCameraSupport && vm.isDeviceDetect" layout-fill>
  20 + <div ng-show="!vm.isShowCamera" layout="column" layout-align="space-between center" layout-fill>
  21 + <div class="tb-web-camera__last-photo" flex>
  22 + <span ng-show="!vm.ctx.data[0].data[0][1]" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span>
  23 + <img ng-show="vm.ctx.data[0].data[0][1]" class="tb-web-camera__last-photo_img" ng-src="{{vm.ctx.data[0].data[0][1]}}" />
  24 + </div>
  25 + <md-button class="md-raised md-primary" ng-click="vm.takePhoto()"">{{ "widgets.input-widgets.take-photo" | translate }}</md-button>
  26 + </div>
  27 + <div ng-show="vm.isShowCamera" layout="column" layout-align="center center" id="photoCamera">
  28 + <div class="camera" ng-show="!vm.isPreviewPhoto">
  29 + <video autoplay muted playsinline id="videoStream" class="camera-stream"></video>
  30 + <div class="camera-controls" layout="row" layout-wrap="" layout-align="space-between end">
  31 + <div flex></div>
  32 + <md-button class="md-fab md-primary md-mini" aria-label="{{ 'widgets.input-widgets.switch-camera' | translate }}"
  33 + ng-click="vm.switchWebCamera()" ng-disabled="{{vm.videoInput.length < 2}}">
  34 + <md-icon md-font-icon="switch_camera" aaria-label="{{ 'widgets.input-widgets.switch-camera' | translate }}">switch_camera</md-icon>
  35 + </md-button>
  36 + <md-button class="md-fab md-hue-2" aria-label="{{ 'widgets.input-widgets.take-photo' | translate }}" ng-click="vm.createPhoto()">
  37 + <md-icon md-font-icon="photo_camera" aria-label="Take Photo">photo_camera</md-icon>
  38 + </md-button>
  39 + <md-button class="md-fab md-primary md-mini" aria-label="{{ 'action.cancel' | translate }}" ng-click="vm.closeCamera()">
  40 + <md-icon md-font-icon="close" aria-label="{{ 'action.cancel' | translate }}">close</md-icon>
  41 + </md-button>
  42 + <div flex></div>
  43 + </div>
  44 + </div>
  45 + <div class="camera" ng-show="vm.isPreviewPhoto">
  46 + <img class="camera-stream" ng-src="{{vm.previewPhoto}}">
  47 + <canvas style="display:none;"></canvas>
  48 + <div class="camera-controls" layout="row" layout-wrap="" layout-align="space-between end">
  49 + <div flex></div>
  50 + <md-button class="md-fab md-primary" aria-label="{{ 'action.cancel' | translate }}" ng-click="vm.cancelPhoto()">
  51 + <md-icon md-font-icon="close" aria-label="{{ 'action.cancel' | translate }}">close</md-icon>
  52 + </md-button>
  53 + <md-button class="md-fab md-hue-2" aria-label="{{ 'action.save' | translate }}" ng-click="vm.savePhoto()">
  54 + <md-icon md-font-icon="check" aria-label="{{ 'action.save' | translate }}">check</md-icon>
  55 + </md-button>
  56 + <div flex></div>
  57 + </div>
  58 + </div>
  59 + </div>
  60 + </div>
  61 + <div class="message-text" ng-hide="vm.isEntityDetected">
  62 + {{ 'widgets.input-widgets.no-entity-selected' | translate }}
  63 + </div>
  64 + <div class="message-text" ng-if="vm.isEntityDetected && !vm.dataKeyDetected">
  65 + {{ 'widgets.input-widgets.no-datakey-selected' | translate }}
  66 + </div>
  67 + <div class="message-text" ng-if="vm.isEntityDetected && vm.dataKeyDetected && !vm.isCameraSupport">
  68 + {{ 'widgets.input-widgets.no-support-web-camera' | translate }}
  69 + </div>
  70 + <div class="message-text" ng-if="vm.isEntityDetected && vm.dataKeyDetected && vm.isCameraSupport && !vm.isDeviceDetect">
  71 + {{ 'widgets.input-widgets.no-support-web-camera' | translate }}
  72 + </div>
  73 +</div>