diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc
index 6a377420..9ebcc1b3 100644
--- a/nymea-app/resources.qrc
+++ b/nymea-app/resources.qrc
@@ -257,5 +257,11 @@
ui/devicepages/FingerprintReaderDevicePage.qml
ui/images/account.svg
ui/images/contact-new.svg
+ ui/components/SegmentedImage.qml
+ ui/components/SegmentRenderer.qml
+ ui/components/SegmentBoundingBoxes.qml
+ ui/components/FingerprintVisual.qml
+ ui/images/fingerprint/fingerprint_boxes.json
+ ui/images/fingerprint/fingerprint_segmented.png
diff --git a/nymea-app/ui/components/FingerprintVisual.qml b/nymea-app/ui/components/FingerprintVisual.qml
new file mode 100644
index 00000000..ab474788
--- /dev/null
+++ b/nymea-app/ui/components/FingerprintVisual.qml
@@ -0,0 +1,54 @@
+import QtQuick 2.9
+
+SegmentedImage {
+ id: segmentedImage
+
+ property var masks: []
+
+ property bool debug: false
+
+ // http://stackoverflow.com/a/1830844/538866
+ function isNumeric (n) {
+ return !isNaN(parseFloat(n)) && isFinite(n);
+ }
+
+ function getMasksToEnroll () {
+ var outMasks = [];
+ if (masks && masks.length) {
+ masks.forEach(function (mask, i) {
+ // Format is "/[x1,y1,w1,h1],…,[xn,yn,wn,hn]"
+ // If any value is non-numeric, we drop the mask.
+ if (!isNumeric(mask.x) || !isNumeric(mask.y) || !isNumeric(mask.width)
+ || !isNumeric(mask.height))
+ return;
+
+ // Translate the box so as to mirror the mask
+ mask.x = (1 - (mask.x + mask.width));
+
+ outMasks.push(mask);
+ });
+ }
+ return outMasks;
+ }
+
+ onMasksChanged: segmentedImage.enrollMasks(getMasksToEnroll())
+
+ textureSource: "../images/fingerprint/fingerprint_segmented.png"
+ boxesSource: "../images/fingerprint/fingerprint_boxes.json"
+
+ Repeater {
+ model: segmentedImage.masks
+
+ Rectangle {
+ visible: segmentedImage.debug
+ color: "red"
+ opacity: 0.25
+ x: modelData.x * segmentedImage.implicitWidth
+ y: modelData.y * segmentedImage.implicitHeight
+ width: modelData.width * segmentedImage.implicitWidth
+ height: modelData.height * segmentedImage.implicitHeight
+
+ Component.onCompleted: console.log('Scanner mask (x, y, w, h):', x, y, width, height)
+ }
+ }
+}
diff --git a/nymea-app/ui/components/SegmentBoundingBoxes.qml b/nymea-app/ui/components/SegmentBoundingBoxes.qml
new file mode 100644
index 00000000..2ae27aa2
--- /dev/null
+++ b/nymea-app/ui/components/SegmentBoundingBoxes.qml
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ *
+ * Authored by Florian Boucault
+ */
+
+import QtQuick 2.4
+
+QtObject {
+ id: segmentBoundingBoxes
+
+ property string source
+ onSourceChanged: parseBoundingBoxes(source)
+
+ property var boundingBoxes: []
+ property real width
+ property real height
+ property int count: boundingBoxes.length
+
+ // The API cannot be used reliably before this signal has been emitted.
+ signal ready()
+
+ function parseBoundingBoxes(source) {
+ var xhr = new XMLHttpRequest;
+ xhr.open("GET", source);
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == XMLHttpRequest.DONE) {
+ var b = [];
+ var json = JSON.parse(xhr.responseText);
+ boundingBoxes = json["boxes"];
+ width = json["width"];
+ height = json["height"];
+ ready();
+ }
+ }
+ xhr.send();
+ }
+
+ function intersects(box1, box2) {
+ // TODO: optimize
+ var x11 = box1[0];
+ var y11 = box1[1];
+ var x12 = box1[0] + box1[2];
+ var y12 = box1[1] + box1[3];
+ var x21 = box2[0];
+ var y21 = box2[1];
+ var x22 = box2[0] + box2[2];
+ var y22 = box2[1] + box2[3];
+ var x_overlap = Math.max(0, Math.min(x12,x22) - Math.max(x11,x21));
+ var y_overlap = Math.max(0, Math.min(y12,y22) - Math.max(y11,y21));
+ return (x_overlap / Math.min(box1[2], box2[2]) > 0.25
+ && y_overlap / Math.min(box1[3], box2[3]) > 0.25);
+ }
+
+ function computeIntersections(hitBox) {
+ var absoluteHitBox = [hitBox[0] * width, hitBox[1] * height,
+ hitBox[2] * width, hitBox[3] * height];
+
+ var intersections = [];
+ for (var i in boundingBoxes) {
+ var boundingBox = boundingBoxes[i];
+ if (intersects(absoluteHitBox, boundingBox)) {
+ intersections.push(i);
+ }
+ }
+
+ return intersections;
+ }
+}
diff --git a/nymea-app/ui/components/SegmentRenderer.qml b/nymea-app/ui/components/SegmentRenderer.qml
new file mode 100644
index 00000000..ab612a04
--- /dev/null
+++ b/nymea-app/ui/components/SegmentRenderer.qml
@@ -0,0 +1,76 @@
+import QtQuick 2.9
+
+ShaderEffect {
+ id: segmentRenderer
+
+ implicitWidth: texture.width
+ implicitHeight: texture.height
+
+ function animate(segments) {
+ for (var i in segments) {
+ var progressPixel = progressTexture.children[segments[i]];
+ if (progressPixel.progress == 0.0) {
+ progressPixel.animation.start();
+ }
+ }
+ }
+
+ property string source
+ property int segmentsCount
+ property color backgroundColor: app.foregroundColor
+ property color fillColor: app.accentColor
+ property Image texture: Image {
+ source: segmentRenderer.source
+ }
+ property var progressTexture: progressTexture
+ property int progressTextureSize: progressTexture.size
+
+ fragmentShader: "
+ varying mediump vec2 qt_TexCoord0;
+ uniform lowp float qt_Opacity;
+ uniform lowp vec4 backgroundColor;
+ uniform lowp vec4 fillColor;
+ uniform lowp sampler2D texture;
+ uniform lowp sampler2D progressTexture;
+ uniform lowp int progressTextureSize;
+
+ void main() {
+ lowp vec4 p = texture2D(texture, qt_TexCoord0);
+ lowp float segment = p.r * 255.0;
+ lowp vec4 segmentProgress = step(0.9, segment) * texture2D(progressTexture, vec2((segment - 1.0 + 0.5) / float(progressTextureSize), 0.5));
+ lowp vec4 color = mix(fillColor, backgroundColor, step(segmentProgress.r, p.g));
+ gl_FragColor = vec4(color.rgb, 1.0) * p.b * qt_Opacity;
+ }
+ "
+
+ // TODO: not the most efficient; could be replaced with an image provider
+ Row {
+ id: progressTexture
+
+ property int size: 128
+ layer.enabled: true
+ layer.sourceRect: Qt.rect(0, 0, size, 1)
+ layer.textureSize: Qt.size(size, 1)
+ layer.wrapMode: ShaderEffectSource.ClampToEdge
+ visible: false
+
+ Repeater {
+ model: segmentRenderer.segmentsCount
+ Rectangle {
+ id: progressPixel
+ width: 1
+ height: 1
+ color: Qt.rgba(progress, progress, progress, 1.0)
+ property real progress
+ property NumberAnimation animation: NumberAnimation {
+ target: progressPixel
+ property: "progress"
+ from: 0.0
+ to: 1.0
+ duration: 1000
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+ }
+}
diff --git a/nymea-app/ui/components/SegmentedImage.qml b/nymea-app/ui/components/SegmentedImage.qml
new file mode 100644
index 00000000..77c4ea85
--- /dev/null
+++ b/nymea-app/ui/components/SegmentedImage.qml
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ *
+ * Authored by Florian Boucault
+ */
+
+import QtQuick 2.4
+
+Item {
+ id: segImg
+ property alias textureSource: segmentRenderer.source
+ property alias boxesSource: segmentBoundingBoxes.source
+
+ implicitWidth: segmentRenderer.implicitWidth
+ implicitHeight: segmentRenderer.implicitHeight
+
+ // Ready to enroll.
+ signal ready()
+
+ function enrollMasks(masks) {
+ if (masks && masks.length) {
+ var segments = [];
+ masks.forEach(function (mask, i) {
+ var hitBox = [mask.x, mask.y, mask.width, mask.height];
+ segments = segments.concat(segmentBoundingBoxes.computeIntersections(hitBox));
+ });
+ segmentRenderer.animate(segments);
+ }
+ }
+
+ SegmentRenderer {
+ id: segmentRenderer
+ segmentsCount: segmentBoundingBoxes.count
+ }
+
+ SegmentBoundingBoxes {
+ id: segmentBoundingBoxes
+ onReady: segImg.ready()
+ }
+}
diff --git a/nymea-app/ui/customviews/GenericTypeLogView.qml b/nymea-app/ui/customviews/GenericTypeLogView.qml
index b713b0e3..dc136765 100644
--- a/nymea-app/ui/customviews/GenericTypeLogView.qml
+++ b/nymea-app/ui/customviews/GenericTypeLogView.qml
@@ -15,6 +15,8 @@ Item {
property alias delegate: listView.delegate
+ property bool autoscroll: true
+
ColumnLayout {
anchors.fill: parent
@@ -40,7 +42,11 @@ Item {
Layout.fillHeight: true
model: logsModel
clip: true
-// onCountChanged: positionViewAtEnd()
+ onCountChanged: {
+ if (root.autoscroll) {
+ positionViewAtEnd()
+ }
+ }
onContentYChanged: {
if (!logsModel.busy && contentY - originY < 5 * height) {
diff --git a/nymea-app/ui/devicepages/FingerprintReaderDevicePage.qml b/nymea-app/ui/devicepages/FingerprintReaderDevicePage.qml
index b248a147..f5f97ec2 100644
--- a/nymea-app/ui/devicepages/FingerprintReaderDevicePage.qml
+++ b/nymea-app/ui/devicepages/FingerprintReaderDevicePage.qml
@@ -17,39 +17,9 @@ DevicePageBase {
ColumnLayout {
anchors.fill: parent
-// Item {
-// Layout.fillWidth: true
-// Layout.preferredHeight: root.inputVisible ? inputColumn.implicitHeight : 0
-// Behavior on Layout.preferredHeight { NumberAnimation { duration: 130; easing.type: Easing.InOutQuad } }
-
-// ColumnLayout {
-// id: inputColumn
-// anchors { left: parent.left; bottom: parent.bottom; right: parent.right }
-
-// TextField {
-// id: titleTextField
-// Layout.fillWidth: true
-// Layout.topMargin: app.margins
-// Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
-// placeholderText: qsTr("Title")
-// }
-
-// TextArea {
-// id: bodyTextField
-// Layout.fillWidth: true
-// Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
-// placeholderText: qsTr("Text")
-// wrapMode: Text.WordWrap
-// }
-// }
-// }
-
-
-
-
Button {
Layout.fillWidth: true
- Layout.margins: app.margins
+ Layout.leftMargin: app.margins; Layout.topMargin: app.margins; Layout.rightMargin: app.margins
text: qsTr("Manage access")
onClicked: {
pageStack.push(manageUsersComponent)
@@ -69,12 +39,12 @@ DevicePageBase {
live: true
Component.onCompleted: update()
typeIds: [root.accessGrantedEventType.id, root.accessDeniedEventType.id];
-
}
delegate: MeaListItemDelegate {
width: parent.width
- iconName: "../images/notification.svg"
+ iconName: model.typeId === root.accessGrantedEventType.id ? "../images/tick.svg" : "../images/dialog-error-symbolic.svg"
+ iconColor: model.typeId === root.accessGrantedEventType.id ? "green" : "red"
text: model.typeId === root.accessGrantedEventType.id ? qsTr("Access granted for user %1").arg(model.value) : qsTr("Access denied")
subText: Qt.formatDateTime(model.timestamp)
progressive: false
@@ -134,6 +104,28 @@ DevicePageBase {
engine.deviceManager.executeAction(root.device.id, actionType.id, params)
}
}
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ width: 200
+ spacing: app.margins * 2
+ visible: parent.count === 0
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: width
+ FingerprintVisual {
+ id: fingerprintVisual
+ anchors.centerIn: parent
+ scale: parent.height / implicitHeight
+ }
+ }
+
+ Button {
+ text: qsTr("Add a fingerprint")
+ onClicked: pageStack.push(addUserComponent)
+ Layout.fillWidth: true
+ }
+ }
}
}
}
@@ -153,8 +145,8 @@ DevicePageBase {
Connections {
target: engine.deviceManager
onExecuteActionReply: {
- addUserPage.error = params["deviceError"] !== DeviceManager.DeviceErrorNoError
- print("Execute action reply:", params);
+ addUserPage.error = params["deviceError"] !== "DeviceErrorNoError"
+ print("Execute action reply:", params["deviceError"]);
addUserSwipeView.currentIndex++
}
}
@@ -164,6 +156,7 @@ DevicePageBase {
SwipeView {
id: addUserSwipeView
Layout.fillWidth: true
+ Layout.topMargin: app.margins * 2
Layout.preferredHeight: 200
Layout.alignment: Qt.AlignTop
Item {
@@ -182,6 +175,27 @@ DevicePageBase {
id: userIdTextField
Layout.fillWidth: true
}
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Finger")
+ }
+ ComboBox {
+ id: fingerComboBox
+ Layout.fillWidth: true
+ model: ListModel {
+ ListElement { modelData: qsTr("Left thumb"); enumValue: "ThumbLeft" }
+ ListElement { modelData: qsTr("Left index finger"); enumValue: "IndexFingerLeft" }
+ ListElement { modelData: qsTr("Left middle finger"); enumValue: "MiddleFingerLeft" }
+ ListElement { modelData: qsTr("Left ring finger"); enumValue: "RingFingerLeft" }
+ ListElement { modelData: qsTr("Left pinky finger"); enumValue: "PinkyLeft" }
+ ListElement { modelData: qsTr("Right thumb"); enumValue: "ThumbRight" }
+ ListElement { modelData: qsTr("Right index finger"); enumValue: "IndexFingerRight" }
+ ListElement { modelData: qsTr("Right middle finger"); enumValue: "MiddleFingerRight" }
+ ListElement { modelData: qsTr("Right ring finger"); enumValue: "RingFingerRight" }
+ ListElement { modelData: qsTr("Right pinky finger"); enumValue: "PinkyRight" }
+ }
+ }
+
Button {
text: qsTr("Add user")
Layout.fillWidth: true
@@ -192,6 +206,10 @@ DevicePageBase {
titleParam["paramTypeId"] = actionType.paramTypes.findByName("userId").id
titleParam["value"] = userIdTextField.displayText
params.push(titleParam)
+ var fingerParam = {}
+ fingerParam["paramTypeId"] = actionType.paramTypes.findByName("finger").id
+ fingerParam["value"] = fingerComboBox.model.get(fingerComboBox.currentIndex).enumValue
+ params.push(fingerParam)
engine.deviceManager.executeAction(root.device.id, actionType.id, params)
addUserSwipeView.currentIndex++
}
@@ -206,9 +224,43 @@ DevicePageBase {
ColumnLayout {
anchors.fill: parent
anchors.margins: app.margins
+ spacing: app.margins * 2
Label {
text: qsTr("Please scan the fingerprint now")
Layout.fillWidth: true
+ font.pixelSize: app.largeFont
+ color: app.accentColor
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Item {
+ Layout.preferredWidth: 200
+ Layout.preferredHeight: 200
+ Layout.alignment: Qt.AlignCenter
+
+ FingerprintVisual {
+ id: fingerprintVisual
+ scale: parent.height / implicitHeight
+
+ anchors.centerIn: parent
+ Timer {
+ interval: 500
+ property real position: 0
+ running: addUserSwipeView.currentIndex == 1
+ repeat: true
+ onTriggered: {
+ var masks = [];
+ masks.push({x: 0, y: 0, width: 1, height: position})
+ position += 0.1
+ if (position < 1.1) {
+ fingerprintVisual.masks = masks
+ } else {
+ position = 0
+ fingerprintVisual.masks = []
+ }
+ }
+ }
+ }
}
}
}
diff --git a/nymea-app/ui/images/fingerprint/fingerprint_boxes.json b/nymea-app/ui/images/fingerprint/fingerprint_boxes.json
new file mode 100644
index 00000000..657afc9b
--- /dev/null
+++ b/nymea-app/ui/images/fingerprint/fingerprint_boxes.json
@@ -0,0 +1 @@
+{"width": 451, "boxes": [[291, 515, 104, 50], [269, 510, 96, 34], [227, 506, 62, 16], [198, 505, 72, 60], [144, 498, 46, 67], [200, 467, 133, 32], [214, 449, 87, 28], [67, 432, 25, 104], [396, 420, 31, 89], [177, 395, 22, 103], [133, 390, 15, 96], [266, 375, 8, 31], [1, 356, 7, 42], [336, 355, 27, 114], [221, 350, 95, 105], [243, 323, 17, 106], [155, 317, 68, 248], [110, 317, 43, 243], [22, 314, 12, 155], [199, 310, 13, 127], [302, 296, 83, 223], [276, 295, 20, 136], [311, 275, 30, 189], [177, 266, 8, 114], [88, 263, 34, 287], [221, 249, 9, 86], [441, 238, 9, 169], [110, 231, 9, 70], [44, 226, 16, 280], [310, 225, 8, 181], [199, 223, 53, 86], [370, 217, 37, 288], [249, 208, 25, 153], [177, 201, 61, 49], [281, 195, 15, 85], [133, 183, 16, 196], [177, 178, 96, 29], [155, 167, 35, 134], [345, 166, 18, 173], [200, 156, 81, 31], [1, 149, 17, 192], [386, 145, 18, 60], [305, 142, 36, 118], [66, 139, 26, 277], [149, 134, 168, 77], [111, 132, 45, 85], [88, 130, 36, 120], [350, 118, 35, 162], [165, 111, 134, 28], [262, 92, 85, 64], [414, 91, 35, 133], [129, 89, 119, 36], [22, 88, 42, 211], [92, 67, 256, 62], [47, 64, 75, 149], [349, 47, 80, 360], [132, 44, 257, 90], [13, 41, 56, 95], [66, 22, 273, 60], [198, 1, 215, 76], [74, 1, 109, 39]], "height": 566}
diff --git a/nymea-app/ui/images/fingerprint/fingerprint_segmented.png b/nymea-app/ui/images/fingerprint/fingerprint_segmented.png
new file mode 100644
index 00000000..b14e672f
Binary files /dev/null and b/nymea-app/ui/images/fingerprint/fingerprint_segmented.png differ