/* Speed Climbing Stopwatch - Simple Stopwatch for Climbers Copyright (C) 2018 Itsblue Development - Dorian Zeder This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License. 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import QtQuick 2.9 import QtMultimedia 5.8 import QtQuick.Window 2.2 import QtQuick.Controls 2.4 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 import com.itsblue.speedclimbingstopwatch 1.0 import "./components" Popup { id: root modal: true dim: false opacity: 0 enter: Transition { NumberAnimation { properties: "opacity"; to: 1; duration: 300; easing.type: Easing.InOutQuad } NumberAnimation { properties: "scale"; from: 0.9; to: 1; duration: 300; easing.type: Easing.InOutQuad } } exit: Transition { NumberAnimation { properties: "opacity"; to: 0; duration: 300; easing.type: Easing.InOutQuad } NumberAnimation { properties: "scale"; from: 1; to: 0.9; duration: 300; easing.type: Easing.InOutQuad } } background: Item { RectangularGlow { id: backgroundEffect glowRadius: 7 spread: 0.02 color: "black" opacity: 0.18 anchors.fill: backgroundRect cornerRadius: backgroundRect.radius scale: 1 } Rectangle { id: backgroundRect anchors.fill: parent radius: width * 0.1 color: appTheme.style.viewColor } } StackView { id: profiles_stack property int text_pixelSize: headlineUnderline.width * 0.08 //initialItem: profileListComp width: headlineUnderline.width anchors { top: topContainerItm.bottom left: parent.left leftMargin: ( parent.width - headlineUnderline.width ) / 2 topMargin: headlineUnderline.anchors.topMargin * 1.2 bottom: parent.bottom bottomMargin: topContainerItm.height * 0.3 } Behavior on opacity { NumberAnimation {duration: 200} } Component.onCompleted: { profiles_stack.init() } Connections { target: root onOpened: { profiles_stack.init() } } onCurrentItemChanged: { currentItem.opened() } function init() { if(profiles_stack.depth === 0){ profiles_stack.openAthletes() } else { profiles_stack.currentItem.opened() } } function openAthletes() { var athsComp = profileListComp.createObject(null, {}) profiles_stack.push(athsComp) } function openResults( userName ){ var resComp = resultViewComp.createObject(null, {"userName": userName}) profiles_stack.push(resComp) } /*-----List of all profiles-----*/ Component { id: profileListComp RemoteDataListView { id: profileList property int currentAthlete: -1 property string title: "profiles" property string secondButt: "add" signal opened() onOpened: { profileList.loadData() } //anchors.fill: parent //anchors.topMargin: topContainerItm.height * 0.1 loadData: function () { status = 905 //listData = {} var retData = speedBackend.getAthletes() if(retData === undefined){ status = 500 return } listData = retData["allAthletes"] currentAthlete = retData["activeAthlete"] status = listData.lenght !== false ? 200:0 } delegate: SwipeDelegate { id: swipeDelegate property bool active: profileList.currentAthlete === profileList.listData[index]["id"] text: profileList.listData[index]["fullName"] width: profileList.width - (swipeDelegate.x) height: profileList.height / 5 font.pixelSize: profiles_stack.text_pixelSize function remove() { removeAnim.start() } onClicked: { profiles_stack.openResults(profileList.listData[index]["userName"]) } contentItem: Text { visible: false } Text { anchors { verticalCenter: parent.verticalCenter left: parent.left leftMargin: swipeDelegate.width * 0.05 right: parent.right rightMargin: swipeDelegate.rightPadding } text: swipeDelegate.text color: appTheme.style.textColor verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignLeft fontSizeMode: Text.Fit font.pixelSize: swipeDelegate.height * 0.4 minimumPixelSize: 1 } background: Rectangle { color: pressed ? appTheme.style.delegatePressedColor : appTheme.style.delegateBackgroundColor Behavior on color { ColorAnimation { duration: 200 } } } CheckBox { id: control anchors { verticalCenter: parent.verticalCenter right: parent.right rightMargin: 7 } height: parent.height * 0.6 checked: swipeDelegate.active onCheckedChanged: { if(checked && !swipeDelegate.active && speedBackend.selectAthlete(profileList.listData[index]["userName"])){ profileList.loadData() } } indicator: Rectangle { implicitWidth: 26 implicitHeight: 26 height: parent.height width: height x: control.leftPadding y: parent.height / 2 - height / 2 radius: width * 0.2 border.color: control.down ? "#17a81a" : "#21be2b" color: control.down ? appTheme.style.delegatePressedColor : appTheme.style.delegateBackgroundColor Rectangle { width: parent.width * 0.65 height: width anchors.centerIn: parent radius: control.checked ? width * 0.2:0 color: control.down ? "#17a81a" : "#21be2b" opacity: control.checked ? 1:0 scale: control.checked ? 0.9:0 Behavior on color { ColorAnimation { duration: 200 } } Behavior on radius { NumberAnimation { duration: 200 } } Behavior on opacity { NumberAnimation { duration: 200 } } Behavior on scale { NumberAnimation { duration: 200 } } } } } Rectangle { color: "grey" height: 1 width: parent.width * 0.9 visible: index > 0 anchors { horizontalCenter: parent.horizontalCenter top: parent.top } } NumberAnimation { id: removeAnim target: swipeDelegate property: "height" to: 0 easing.type: Easing.InOutQuad onStopped: profileModel.model.remove(index) } swipe.transition: Transition { SmoothedAnimation { velocity: 3; easing.type: Easing.InOutCubic } } swipe.left: Row { anchors.left: parent.left height: parent.height Label { id: deleteLabel text: qsTr("Delete") color: appTheme.style.textColor verticalAlignment: Label.AlignVCenter padding: 12 height: parent.height SwipeDelegate.onClicked: { profileList.status = 905 if(speedBackend.deleteAthlete(profileList.listData[index]["userName"])){ profileList.loadData() return } profileList.status = 200 } background: Rectangle { color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) : "tomato" } } } } } } /*-----Option to add a profile-----*/ Component { id: addProfileComp Column { property string title: "add profile" property string secondButt: "ok" property string newProfileName: "" Connections { target: head_add enabled: true onClicked: { if(speedBackend.createAthlete(userNameTf.text, fullNameTf.text)){ profiles_stack.get(profiles_stack.depth - 2 ).opened() profiles_stack.pop() } } } TextField { id: fullNameTf width: parent.width placeholderText: "full name" onTextChanged: { parent.newProfileName = text } Keys.onReturnPressed: { } } TextField { id: userNameTf width: parent.width placeholderText: "username" onTextChanged: { parent.newProfileName = text } Keys.onReturnPressed: { } } } } // --- Result View --- Component { id: resultViewComp RemoteDataListView { id: resultView property string userName property string title: userName property string secondButt: "none" signal opened() anchors.margins: 10 clip: true onOpened: { loadData() } loadData: function () { status = 905 listData = {} listData = speedBackend.getResults(userName) status = listData.lenght !== false ? 200:0 } delegate: SmoothItemDelegate { id: resultDel width: parent.width height: resultView.height / 4 backgroundRect.radius: 0 function getDateText(){ var date = new Date(listData[index]["timestamp"]*1000).toLocaleString(Qt.locale(), "dddd, dd.MMM HH:mm") return date } Rectangle { color: "grey" height: 1 width: parent.width * 0.9 visible: index > 0 anchors { horizontalCenter: parent.horizontalCenter top: parent.top } } Column { anchors.fill: parent anchors.leftMargin: parent.width * 0.05 Label { id: dateLa height: parent.height / parent.children.length font.pixelSize: height * 0.8 fontSizeMode: Text.Fit color: appTheme.style.textColor text: resultDel.getDateText() } Label { id: resultLa height: parent.height / parent.children.length font.pixelSize: height * 0.8 fontSizeMode: Text.Fit color: appTheme.style.textColor text: qsTr("result: ") + (listData[index]["result"] / 1000).toFixed(3) + " s" } Label { id: reactionTimeLa height: parent.height / parent.children.length font.pixelSize: height * 0.8 fontSizeMode: Text.Fit color: appTheme.style.textColor text: qsTr("reaction time: ") + listData[index]["reactionTime"].toFixed(0) + " ms" } } } } } /*-----Custom animations-----*/ property int animationDuration: 200 pushEnter: Transition { NumberAnimation { property: "opacity" from: 0 to: 1 duration: profiles_stack.animationDuration easing.type: Easing.InOutQuad } /*NumberAnimation { property: "x" from: width * 0.1 to: 0 duration: 300 }*/ NumberAnimation { property: "scale" from: 1.1 to: 1 duration: profiles_stack.animationDuration } } pushExit: Transition { NumberAnimation { property: "opacity" from: 1 to: 0 duration: profiles_stack.animationDuration easing.type: Easing.InOutQuad } /*NumberAnimation { property: "x" to: -width * 0.1 from: 0 duration: 300 }*/ NumberAnimation { property: "scale" from: 1 to: 0.9 duration: profiles_stack.animationDuration } } popExit: Transition { NumberAnimation { property: "opacity" from: 1 to: 0 duration: profiles_stack.animationDuration easing.type: Easing.InOutQuad } /*NumberAnimation { property: "x" to: width * 0.1 from: 0 duration: 300 }*/ NumberAnimation { property: "scale" from: 1 to: 1.1 duration: profiles_stack.animationDuration } } popEnter: Transition { NumberAnimation { property: "opacity" from: 0 to: 1 duration: profiles_stack.animationDuration easing.type: Easing.InOutQuad } /*NumberAnimation { property: "x" from: -width * 0.1 to: 0 duration: 300 }*/ NumberAnimation { property: "scale" from: 0.9 to: 1 duration: profiles_stack.animationDuration } } } Item { id: topContainerItm anchors { top: parent.top horizontalCenter: parent.horizontalCenter } height: parent.height * 0.15 width: backgroundRect.width RectangularGlow { id: headerUnderlineEffect glowRadius: 7 spread: 0.02 color: "black" opacity: 0.18 anchors.fill: headlineUnderline scale: 1 } Rectangle { id: headlineUnderline height: 1 width: parent.width color: "grey" anchors { bottom: parent.bottom left: parent.left right: parent.right } } Canvas { id: headerBackground anchors.fill: parent property color color: appTheme.style.viewColor onPaint: { var ctx = getContext("2d"); var topMargin = backgroundRect.radius ctx.beginPath(); ctx.fillStyle = headerBackground.color ctx.moveTo(width, topMargin); // //ctx.lineTo(width, topMargin); ctx.lineTo(width, height); ctx.lineTo(0, height); ctx.lineTo(0, topMargin) ctx.arc(topMargin, topMargin, topMargin, 1 * Math.PI, 1.5*Math.PI, false); ctx.lineTo(width-topMargin, 0) ctx.arc(width-topMargin, topMargin, topMargin, 1.5*Math.PI, 0, false) ctx.fill(); } } Label { id: head_text anchors { centerIn: parent } width: parent.width * 0.8 height: parent.height * 0.8 fontSizeMode: Text.Fit font.pixelSize: headlineUnderline.width * 0.1 minimumPixelSize: 0 verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter color: appTheme.style.textColor text: profiles_stack.currentItem.title } } FancyButton { id: head_back anchors { left: parent.left leftMargin: -height * 0.3 top:parent.top topMargin: anchors.leftMargin } height: topContainerItm.height * 0.8 width: height glowOpacity: Math.pow( root.opacity, 100 ) backgroundColor: appTheme.style.buttonColor image: appTheme.style.backIcon onClicked: profiles_stack.depth > 1 ? profiles_stack.pop():root.close() } FancyButton { id: head_add anchors { right: parent.right rightMargin: -height * 0.3 top:parent.top topMargin: anchors.rightMargin } height: topContainerItm.height * 0.8 width: height opacity: root.opacity < 1 ? root.opacity : ["ok", "add"].indexOf(profiles_stack.currentItem.secondButt) >= 0 ? 1:0 glowOpacity: opacity < 1 ? Math.pow( opacity, 100 ) : Math.pow( opacity, 100 ) backgroundColor: appTheme.style.buttonColor image: appTheme.style.confirmIcon imageScale: profiles_stack.currentItem.secondButt === "ok" ? 1:0 Label { anchors { top: parent.top topMargin: parent.height/2 - height*0.55 left: parent.left leftMargin: parent.width/2 - width/2 } opacity: profiles_stack.currentItem.secondButt === "add" ? 1:0 color: appTheme.style.textColor text: "+" font.pixelSize: parent.height * 0.8 } onClicked: { switch(profiles_stack.currentItem.secondButt){ case "add": profiles_stack.push(addProfileComp) break case "ok": //speedBackend.createAthlete(fullNameTf.text, userNameTf.text) } } Behavior on opacity { enabled: root.opacity === 1 NumberAnimation { duration: 200 } } } }