Commit b6a3e7121568a3d31b112fdc0a3a81edf2e95ff4
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
Showing
6 changed files
with
360 additions
and
1 deletions
... | ... | @@ -276,6 +276,22 @@ |
276 | 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 | 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 | 25 | import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget'; |
26 | 26 | import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator'; |
27 | 27 | import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget'; |
28 | +import thingsboardWebCameraInputWidget from '../widget/lib/web-camera-input-widget'; | |
28 | 29 | |
29 | 30 | import thingsboardRpcWidgets from '../widget/lib/rpc'; |
30 | 31 | |
... | ... | @@ -50,7 +51,8 @@ import thingsboardUtils from '../common/utils.service'; |
50 | 51 | export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, |
51 | 52 | thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, |
52 | 53 | thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget, |
53 | - thingsboardMultipleInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget]) | |
54 | + thingsboardMultipleInputWidget, thingsboardWebCameraInputWidget, thingsboardRpcWidgets, thingsboardTypes, | |
55 | + thingsboardUtils, TripAnimationWidget]) | |
54 | 56 | .factory('widgetService', WidgetService) |
55 | 57 | .name; |
56 | 58 | ... | ... |
... | ... | @@ -1693,10 +1693,15 @@ |
1693 | 1693 | "entity-timeseries-required": "Entity timeseries is required", |
1694 | 1694 | "not-allowed-entity": "Selected entity cannot have shared attributes", |
1695 | 1695 | "no-attribute-selected": "No attribute is selected", |
1696 | + "no-datakey-selected": "No datakey is selected", | |
1696 | 1697 | "no-entity-selected": "No entity selected", |
1698 | + "no-image": "No image", | |
1699 | + "no-support-web-camera": "No supported web camera", | |
1697 | 1700 | "no-timeseries-selected": "No timeseries selected", |
1698 | 1701 | "switch-attribute-value": "Switch entity attribute value", |
1702 | + "switch-camera": "Switch camera", | |
1699 | 1703 | "switch-timeseries-value": "Switch entity timeseries value", |
1704 | + "take-photo": "Take photo", | |
1700 | 1705 | "time": "Time", |
1701 | 1706 | "timeseries-not-allowed": "Timeseries parameter cannot be used in this widget", |
1702 | 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> | ... | ... |