diff --git a/.gitignore b/.gitignore index 058c4f2..3f6455d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /speedclimbing_stopwatch.pro.user .DS_Store +*.pro.user* diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..b55f7c0 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,45 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added +- added profiles dialog + +## [0.04] - 2018-08-11 +### Added +- buzzer icon in the upper left corner indicating that the buzzer is connected +### Fixed +- start seqnece continues in a buggy way when cancel is being pressed while 'at your marks' or 'ready' +- bug that made the start sequence freeze if a delay of zero or lower or a non valid number was set as delay +### Changed +- increased the size of the back buttons in settings / profiles dialog + +## [0.03 - BETA] - 2018-07-29 +### Added +- cancel button during start sequence +- new screen in landscape mode +- buttons for settings and profiles +- the screen stays on now +- the volume csontrols control the media volume directly +- settings dialog +- capabilitie to connect to a Buzzer via Wifi +- it is now possible to setup an automatic start sequence that spells the command +'at your marks' and 'ready' with a customizable delay before them +### Fixed +- bug that made a Button freeze when it was pressed and the screen rotated at the same time + +## [0.02] - 2018-07-18 +### Fixed +- negative time when the stopping starts +- removed delay between the end of the startton an the begin of the stopping +### Changed +- slowed down animations +### Added +- animation for the text "click start to start" between STOPPED and IDLE to + prevent it from getting out of the screen + +## [0.01] +### Initial Release diff --git a/graphics/BaseStation.xcf b/graphics/BaseStation.xcf new file mode 100644 index 0000000..a4d75f3 Binary files /dev/null and b/graphics/BaseStation.xcf differ diff --git a/graphics/Buzzer.xcf b/graphics/Buzzer.xcf index 840ecfb..2cac802 100644 Binary files a/graphics/Buzzer.xcf and b/graphics/Buzzer.xcf differ diff --git a/graphics/icons/BaseStation.png b/graphics/icons/BaseStation.png new file mode 100644 index 0000000..1956399 Binary files /dev/null and b/graphics/icons/BaseStation.png differ diff --git a/graphics/icons/BaseStation_black.png b/graphics/icons/BaseStation_black.png new file mode 100644 index 0000000..6c7e61e Binary files /dev/null and b/graphics/icons/BaseStation_black.png differ diff --git a/graphics/icons/error.png b/graphics/icons/error.png new file mode 100644 index 0000000..7c64f33 Binary files /dev/null and b/graphics/icons/error.png differ diff --git a/graphics/icons/ok.png b/graphics/icons/ok.png new file mode 100644 index 0000000..e24854f Binary files /dev/null and b/graphics/icons/ok.png differ diff --git a/graphics/icons/startpad.png b/graphics/icons/startpad.png index a541769..b087b85 100644 Binary files a/graphics/icons/startpad.png and b/graphics/icons/startpad.png differ diff --git a/graphics/icons/user.png b/graphics/icons/user.png index 2255aa9..952e933 100644 Binary files a/graphics/icons/user.png and b/graphics/icons/user.png differ diff --git a/graphics/icons/user_black.png b/graphics/icons/user_black.png new file mode 100644 index 0000000..2255aa9 Binary files /dev/null and b/graphics/icons/user_black.png differ diff --git a/graphics/speedclimbing_stopwatch.xcf b/graphics/speedclimbing_stopwatch.xcf index f92c268..1ea0472 100644 Binary files a/graphics/speedclimbing_stopwatch.xcf and b/graphics/speedclimbing_stopwatch.xcf differ diff --git a/graphics/startpad.xcf b/graphics/startpad.xcf index d0cc615..518add8 100644 Binary files a/graphics/startpad.xcf and b/graphics/startpad.xcf differ diff --git a/headers/apptheme.h b/headers/apptheme.h new file mode 100644 index 0000000..e2e683b --- /dev/null +++ b/headers/apptheme.h @@ -0,0 +1,31 @@ +#ifndef APPTHEME_H +#define APPTHEME_H + +#include +#include +#include "appsettings.h" + +class AppTheme : public QObject +{ + Q_OBJECT + Q_PROPERTY(QVariant style READ getStyle NOTIFY styleChanged) +public: + explicit AppTheme(QObject *parent = nullptr); + +private: + QVariant lightTheme; + QVariant darkTheme; + + QVariant * currentTheme; + + +signals: + void styleChanged(); + +public slots: + QVariant getStyle(); + Q_INVOKABLE void changeTheme(); + Q_INVOKABLE void refreshTheme(); +}; + +#endif // APPTHEME_H diff --git a/headers/baseconn.h b/headers/baseconn.h new file mode 100644 index 0000000..8c96b9e --- /dev/null +++ b/headers/baseconn.h @@ -0,0 +1,131 @@ +#ifndef BASECONN_H +#define BASECONN_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "headers/appsettings.h" +#include "headers/speedtimer.h" + +class BaseConn : public QObject +{ + Q_OBJECT + +public: + explicit BaseConn(QObject *parent = nullptr); + + // values for the socket connection + int connection_progress; + QString ip; + ushort port = 3563; + int errors; + int errors_until_disconnect = 4; + + // the current state + QString state; + // can be: + // - 'disconnected' + // - 'connecting' + // - 'connected' + + QVariant connections; + + QString latestReadReply; + + //---general status values---// + + +private: + QDateTime *date; + //to get the current time + + QTcpSocket *socket; + //socket for communication with the extention + + QString readBuffer; + + QSemaphore remoteSessions; + + int nextConnectionId; + + struct waitingRequest { + int id; + QEventLoop * loop; + QJsonObject reply; + }; + + QList waitingRequests; + +signals: + void stateChanged(); + //is emitted, when the connection state changes + + void progressChanged(); + //is emmited during the connection process when the progress changes + + void gotUnexpectedReply(QString reply); + + void connectionsChanged(); + + void connectionSlotReleased(); + + void nextRemoteActionChanged(); + + void nextRemoteActionDelayProgChanged(); + + void gotError(QString error); + +public slots: + + Q_INVOKABLE bool connectToHost(); + //function to connect to the base station + + Q_INVOKABLE bool init(); + Q_INVOKABLE void deInit(); + + Q_INVOKABLE void closeConnection(); + + void gotError(QAbstractSocket::SocketError err); + + // --- socket communication handling --- + + Q_INVOKABLE QVariantMap sendCommand(int header, QJsonValue data = ""); + + // --- helper functions --- + + Q_INVOKABLE int writeRemoteSetting(QString key, QString value); + + Q_INVOKABLE bool refreshConnections(); + + // functions for the qml adapter + QString getIP() const; + void setIP(const QString &ipAdress); + + QString getState() const; + void setState(QString newState); + + int getProgress() const; + + QVariant getConnections(); + +private slots: + void readyRead(); + + void processSocketMessage(QString message); + + void socketReplyRecieved(QString reply); + + void socketStateChanged(QAbstractSocket::SocketState socketState); +}; +extern BaseConn * pGlobalBaseConn; + +#endif // BASECONN_H diff --git a/headers/climbingrace.h b/headers/climbingrace.h new file mode 100644 index 0000000..23f7419 --- /dev/null +++ b/headers/climbingrace.h @@ -0,0 +1,109 @@ +#ifndef CLIMBINGRACE_H +#define CLIMBINGRACE_H + +#include +#include +#include +#include +#include "headers/baseconn.h" +#include "headers/appsettings.h" +#include "headers/speedtimer.h" + +class ClimbingRace : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int state READ getState NOTIFY stateChanged) + Q_PROPERTY(int mode READ getMode NOTIFY modeChanged) + Q_PROPERTY(QVariant timers READ getTimerTextList NOTIFY timerTextChanged) + Q_PROPERTY(QString baseStationState READ getBaseStationState NOTIFY baseStationStateChanged) + Q_PROPERTY(QVariant baseStationConnections READ getBaseStationConnections NOTIFY baseStationConnectionsChanged) + Q_PROPERTY(double nextStartActionDelayProgress READ getNextStartActionDelayProgress NOTIFY nextStartActionDelayProgressChanged) + +public: + explicit ClimbingRace(QObject *parent = nullptr); + + enum raceState { IDLE, STARTING, WAITING, RUNNING, STOPPED }; + raceState state; + + enum raceMode { LOCAL, REMOTE }; + raceMode mode; + +private: + AppSettings * appSettings; + BaseConn * baseConn; + + QMediaPlayer * player; + + QTimer * baseStationSyncTimer; + QTimer * timerTextRefreshTimer; + QTimer * nextStartActionTimer; + + QDateTime *date; + + QList speedTimers; + + int nextStartAction; + // 0 : 'at your marks' + // 1 : 'ready' + // 2 : 'start' + + double nextStartActionDelayProgress; + + // helper vars + QVariantList qmlTimers; + const QStringList remoteSettings = {"ready_en", "ready_delay", "at_marks_en", "at_marks_delay"}; + const QStringList remoteOnlySettings = {"soundVolume"}; + +private slots: + // helper functions + void playSoundsAndStartRace(); + bool playSound(QString path); + void setState(raceState newState); + void refreshMode(); + void refreshTimerText(); + + bool refreshRemoteTimers(); + +signals: + void nextStartActionChanged(int nextStartAction); + void nextStartActionDelayProgressChanged(); + + void stateChanged(int state); + void modeChanged(); + void timerTextChanged(); + void baseStationStateChanged(); + void baseStationConnectionsChanged(); + +public slots: + Q_INVOKABLE int startRace(); + Q_INVOKABLE int stopRace(int type); + Q_INVOKABLE int resetRace(); + + void syncWithBaseStation(); + + // functions for qml + Q_INVOKABLE int getState(); + Q_INVOKABLE int getMode(); + Q_INVOKABLE QVariant getTimerTextList(); + Q_INVOKABLE double getNextStartActionDelayProgress(); + + Q_INVOKABLE void writeSetting(QString key, QVariant value); + Q_INVOKABLE QString readSetting(QString key); + + Q_INVOKABLE bool connectBaseStation(); + Q_INVOKABLE void disconnectBaseStation(); + Q_INVOKABLE QString getBaseStationState(); + Q_INVOKABLE QVariant getBaseStationConnections(); + + // athlete management + Q_INVOKABLE QVariant getAthletes(); + Q_INVOKABLE bool createAthlete( QString userName, QString fullName ); + Q_INVOKABLE bool deleteAthlete( QString userName ); + Q_INVOKABLE bool selectAthlete( QString userName ); + Q_INVOKABLE QVariant getResults( QString userName ); + + Q_INVOKABLE bool reloadBaseStationIpAdress(); +}; + +#endif // CLIMBINGRACE_H diff --git a/headers/speedtimer.h b/headers/speedtimer.h new file mode 100644 index 0000000..a4061da --- /dev/null +++ b/headers/speedtimer.h @@ -0,0 +1,48 @@ +#ifndef SPEEDTIMER_H +#define SPEEDTIMER_H + +#include +#include +#include +#include +#include + +class SpeedTimer : public QObject +{ + Q_OBJECT +public: + explicit SpeedTimer(QObject *parent = nullptr); + + enum timerState { IDLE, STARTING, WAITING, RUNNING, STOPPED, FAILED, CANCELLED }; + timerState state; + + // variables for capturing the time + double startTime; + double stopTime; + double stoppedTime; + double reactionTime; + +signals: + void stateChanged(timerState newState); + void startCanceled(bool falseStart); + +public slots: + bool start(bool force = false); + bool stop(int type, bool force = false); + bool reset(bool force = false); + + void setState(timerState newState); + QString getState(); + double getCurrTime(); + QString getText(); + + //helper functions + + void delay(int mSecs); + + timerState stateFromString(QString state); +private: + QDateTime *date; +}; + +#endif // SPEEDTIMER_H diff --git a/qml/ErrorDialog.qml b/qml/ErrorDialog.qml new file mode 100644 index 0000000..ef7f328 --- /dev/null +++ b/qml/ErrorDialog.qml @@ -0,0 +1,83 @@ +/* + 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.2 +import QtQuick.Layouts 1.3 +import com.itsblue.speedclimbingstopwatch 2.0 + + +Popup { + id: root + x: startButt.x + y: startButt.y + width: startButt.width + height: startButt.height + modal: true + + enter: Transition { + NumberAnimation { properties: "scale"; from: 0; to: 1; duration: 300; easing.type: Easing.Linear } + } + + exit: Transition { + NumberAnimation { properties: "scale"; from: 1; to: 0; duration: 300; easing.type: Easing.Linear } + } + + background: Rectangle { + radius: width * 0.5 + color: appTheme.style.viewColor + border.color: appTheme.style.lineColor + border.width: 1 + + Label { + id: head_text + text: "error" + font.pixelSize: headlineUnderline.width * 0.1 + color: enabled ? appTheme.style.textColor:appTheme.style.disabledTextColor + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: headlineUnderline.anchors.topMargin / 2 - height / 2 + } + } + + Rectangle { + id: headlineUnderline + height: 1 + width: parent.width + color: appTheme.style.lineColor + anchors { + top: parent.top + left: parent.left + right: parent.right + topMargin: parent.height * 0.15 + rightMargin: parent.radius - Math.sqrt(Math.pow(parent.radius,2)-Math.pow(parent.radius-anchors.topMargin,2)) + leftMargin: parent.radius - Math.sqrt(Math.pow(parent.radius,2)-Math.pow(parent.radius-anchors.topMargin,2)) + } + } + + Image { + id: errorIcon + source: "qrc:/graphics/icons/error.png" + anchors.centerIn: parent + height: parent.height * 0.5 + width: height + } + } +} diff --git a/qml/ProfilesDialog/AddProfilePage.qml b/qml/ProfilesDialog/AddProfilePage.qml new file mode 100644 index 0000000..0617d96 --- /dev/null +++ b/qml/ProfilesDialog/AddProfilePage.qml @@ -0,0 +1,67 @@ +/* + Speed Climbing Stopwatch - Simple Stopwatch for Climbers + Copyright (C) 2018 - 2019 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" + +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)){ + profilesStack.get(profilesStack.depth - 2 ).opened() + profilesStack.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: { + } + } +} + diff --git a/qml/ProfilesDialog/ProfileListPage.qml b/qml/ProfilesDialog/ProfileListPage.qml new file mode 100644 index 0000000..ae81eb8 --- /dev/null +++ b/qml/ProfilesDialog/ProfileListPage.qml @@ -0,0 +1,237 @@ +/* + Speed Climbing Stopwatch - Simple Stopwatch for Climbers + Copyright (C) 2018 - 2019 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" + + +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: profilesStack.text_pixelSize + + function remove() { + removeAnim.start() + } + + onClicked: { + profilesStack.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" + } + } + } + } +} diff --git a/qml/ProfilesDialog/ProfilesDialog.qml b/qml/ProfilesDialog/ProfilesDialog.qml new file mode 100644 index 0000000..71fea9e --- /dev/null +++ b/qml/ProfilesDialog/ProfilesDialog.qml @@ -0,0 +1,263 @@ +/* + 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 + } + } + + ProfilesStack { + id: profilesStack + + width: headlineUnderline.width + + anchors { + top: topContainerItm.bottom + left: parent.left + leftMargin: ( parent.width - width ) / 2 + topMargin: headlineUnderline.anchors.topMargin * 1.2 + bottom: parent.bottom + bottomMargin: topContainerItm.height * 0.3 + } + + Behavior on opacity { + NumberAnimation {duration: 200} + } + + Component.onCompleted: { + profilesStack.init() + } + + Connections { + target: root + onOpened: { + profilesStack.init() + } + } + } + + 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: 1 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: appTheme.style.textColor + + text: profilesStack.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: profilesStack.depth > 1 ? profilesStack.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(profilesStack.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: profilesStack.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: profilesStack.currentItem.secondButt === "add" ? 1:0 + + color: appTheme.style.textColor + + text: "+" + font.pixelSize: parent.height * 0.8 + } + + onClicked: { + switch(profilesStack.currentItem.secondButt){ + case "add": + profilesStack.createAthlete() + break + case "ok": + //speedBackend.createAthlete(fullNameTf.text, userNameTf.text) + } + + } + + Behavior on opacity { + enabled: root.opacity === 1 + NumberAnimation { + duration: 200 + } + } + } + +} diff --git a/qml/ProfilesDialog/ProfilesStack.qml b/qml/ProfilesDialog/ProfilesStack.qml new file mode 100644 index 0000000..59fb0a4 --- /dev/null +++ b/qml/ProfilesDialog/ProfilesStack.qml @@ -0,0 +1,176 @@ +/* + Speed Climbing Stopwatch - Simple Stopwatch for Climbers + Copyright (C) 2018 - 2019 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" + +StackView { + id: profilesStack + property int text_pixelSize: width * 0.08 + //initialItem: profileListComp + + onCurrentItemChanged: { + currentItem.opened() + } + + function init() { + if(profilesStack.depth === 0){ + profilesStack.openAthletes() + } + else { + profilesStack.currentItem.opened() + } + } + + function openAthletes() { + var athsComp = profileListComp.createObject(null, {}) + profilesStack.push(athsComp) + } + + function openResults( userName ){ + var resComp = resultViewComp.createObject(null, {"userName": userName}) + profilesStack.push(resComp) + } + + function createAthlete() { + var createAthleteComp = addProfileComp.createObject(null, {}) + profilesStack.push(createAthleteComp) + } + + /*-----List of all profiles-----*/ + Component { + id: profileListComp + + ProfileListPage {} + } + + /*-----Option to add a profile-----*/ + Component { + id: addProfileComp + + AddProfilePage {} + } + + // --- Result View --- + Component { + id: resultViewComp + ResultListPage {} + } + + /*-----Custom animations-----*/ + property int animationDuration: 200 + pushEnter: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: profilesStack.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: profilesStack.animationDuration + } + } + pushExit: Transition { + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: profilesStack.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: profilesStack.animationDuration + } + } + + popExit: Transition { + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: profilesStack.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: profilesStack.animationDuration + } + } + popEnter: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: profilesStack.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: profilesStack.animationDuration + } + } +} diff --git a/qml/ProfilesDialog/ResultListPage.qml b/qml/ProfilesDialog/ResultListPage.qml new file mode 100644 index 0000000..30f5fbd --- /dev/null +++ b/qml/ProfilesDialog/ResultListPage.qml @@ -0,0 +1,119 @@ +/* + Speed Climbing Stopwatch - Simple Stopwatch for Climbers + Copyright (C) 2018 - 2019 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" + +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(){ + return new Date(listData[index]["timestamp"]*1000).toLocaleString(Qt.locale(), "dddd, dd.MMM HH:mm") + } + + 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" + } + } + } +} + diff --git a/qml/SettingsDialog.qml b/qml/SettingsDialog.qml index e553157..b71938f 100644 --- a/qml/SettingsDialog.qml +++ b/qml/SettingsDialog.qml @@ -20,8 +20,9 @@ import QtMultimedia 5.8 import QtQuick.Window 2.2 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + import QtQuick.Controls.Styles 1.4 import "./components" -import "./styles" Popup { id: root @@ -31,7 +32,7 @@ Popup { height: startButt.height modal: true dim: false - scale: 0 + opacity: 0 property var connections @@ -56,28 +57,106 @@ Popup { } enter: Transition { - NumberAnimation { properties: "scale"; to: 1; duration: 300; easing.type: Easing.Linear } + 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: "scale"; to: 0; duration: 300; easing.type: Easing.Linear } + 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: Rectangle { radius: width * 0.5 - color: StyleSettings.viewColor - border.color: StyleSettings.lineColor - border.width: 1 + color: appTheme.style.viewColor + border.color: appTheme.style.lineColor + border.width: 0 + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + + RectangularGlow { + id: headerUnderlineEffect + glowRadius: 7 + spread: 0.02 + color: "black" + opacity: 0.18 + anchors.fill: headlineUnderline + scale: 1 + } + + Canvas { + + id: headerBackground - Label { - id: head_text - text: options_stack.currentItem.title - font.pixelSize: headlineUnderline.width * 0.1 - color: enabled ? StyleSettings.textColor:StyleSettings.disabledTextColor anchors { - horizontalCenter: parent.horizontalCenter + left: parent.left + right: parent.right top: parent.top - topMargin: headlineUnderline.anchors.topMargin / 2 - height / 2 + bottom: headlineUnderline.bottom + } + + height: header.height + width: header.width + + property color color: appTheme.style.viewColor + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + + onColorChanged: { + requestPaint() + } + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + var centreX = root.width / 2; + var centreY = root.height / 2; + + ctx.beginPath(); + ctx.fillStyle = headerBackground.color + ctx.moveTo(centreX, centreY); + ctx.arc(centreX, centreY, root.width / 2, 1 * Math.PI, 2*Math.PI, false); + //ctx.lineTo(centreX, centreY); + ctx.fill(); + } + } + + Item { + id: header + + anchors { + left: parent.left + right: parent.right + top: parent.top + bottom: headlineUnderline.bottom + } + + Label { + id: head_text + + anchors { + centerIn: parent + } + + width: headlineUnderline.width * 0.4 + height: parent.height + + fontSizeMode: Text.Fit + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + text: options_stack.currentItem.title + font.pixelSize: headlineUnderline.width * 0.1 + color: enabled ? appTheme.style.textColor:appTheme.style.disabledTextColor } } @@ -85,7 +164,8 @@ Popup { id: headlineUnderline height: 1 width: parent.width - color: StyleSettings.lineColor + color: appTheme.style.lineColor + visible: false anchors { top: parent.top left: parent.left @@ -96,7 +176,7 @@ Popup { } } - Button { + FancyButton { id: head_back anchors { left: parent.left @@ -107,24 +187,15 @@ Popup { height: parent.height * 0.13 width: height - opacity: root.closePolicy === Popup.NoAutoClose ? 0:1 + //opacity: root.closePolicy === Popup.NoAutoClose ? 0:1 enabled: opacity > 0 - background: Rectangle { - radius: width * 0.5 - color: parent.pressed ? StyleSettings.buttonPressedColor:StyleSettings.buttonColor - border.color: StyleSettings.buttonBorderColor - border.width: 1 - Image { - anchors.fill: parent - anchors.margins: parent.width * 0.2 - source: StyleSettings.backIcon + glowOpacity: Math.pow( root.opacity, 100 ) - } - } + image: appTheme.style.backIcon onClicked: { - options_stack.depth > 1 ? options_stack.pop():root.close() + options_stack.depth > 1 ? options_stack.pop():root.close() } Behavior on opacity { @@ -133,12 +204,12 @@ Popup { } } } - } StackView { id: options_stack - property int text_pixelSize: root.height * 0.06 + property int delegateHeight: height * 0.2 + property int rowSpacing: height * 0.01 initialItem: settings width: headlineUnderline.width @@ -148,7 +219,7 @@ Popup { top: parent.top left: parent.left leftMargin: ( parent.width - headlineUnderline.width ) / 2 - topMargin: headlineUnderline.anchors.topMargin * 0.8 + topMargin: headlineUnderline.anchors.topMargin * 0.95 bottom: parent.bottom } @@ -161,138 +232,50 @@ Popup { id: settings Column { - property string title: qsTr("Options") id: settings_col + property string title: qsTr("Options") + spacing: options_stack.rowSpacing + /*----Connect to external devices----*/ - ItemDelegate { + NextPageDelegate { id: connect_del - text: qsTr("connections") - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize - } + height: options_stack.delegateHeight - width: parent.width - - Image { - id: connect_del_image - source: StyleSettings.backIcon - rotation: 180 - height: options_stack.text_pixelSize - width: height - anchors { - verticalCenter: parent.verticalCenter - right: parent.right - rightMargin: 10 - } - } + text: qsTr("Base Station") onClicked: { options_stack.push(connect) } } /*----Automated Start----*/ - ItemDelegate { + NextPageDelegate { id: autostart_del + + height: options_stack.delegateHeight + text: qsTr("start sequence") - width: parent.width - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize - } - - Image { - id: autostart_del_image - source: StyleSettings.backIcon - rotation: 180 - height: options_stack.text_pixelSize - width: height - anchors { - verticalCenter: parent.verticalCenter - right: parent.right - rightMargin: 10 - } - } onClicked: { options_stack.push(autostart) } } /*----Style Settings----*/ - ItemDelegate { - id: style_del - text: qsTr("change style") - width: parent.width - - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize - } - - Image { - id: style_image - source: StyleSettings.backIcon - rotation: 180 - height: options_stack.text_pixelSize - width: height - anchors { - verticalCenter: parent.verticalCenter - right: parent.right - rightMargin: 10 - } - } - onClicked: { - StyleSettings.setTheme() - } - } - } - } - - /*-----Page to connect to extenstions like a startpad or buzzer-----*/ - Component { - id: connect - Column { - id: connect_col - property string title: qsTr("connections") - property int delegateHeight: height*0.18 - ConnectionDelegate { - id: connect_buzz_del - - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize - } - - status: root.connections.buzzer - connect: root.connect - type: "buzzer" + SmoothSwitchDelegate { + id: styleDel + text: qsTr("dark mode") width: parent.width - font.pixelSize: options_stack.text_pixelSize - } + height: options_stack.delegateHeight - ConnectionDelegate { - id: connect_stap_del + checked: speedBackend.readSetting("theme") === "Dark" - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize + onCheckedChanged: { + speedBackend.writeSetting("theme", checked ? "Dark":"Light") + appTheme.refreshTheme() } - - status: root.connections.startpad - connect: root.connect - type: "startpad" - - width: parent.width - font.pixelSize: options_stack.text_pixelSize } } } @@ -302,115 +285,227 @@ Popup { id: autostart Column { id: autostart_col + + spacing: options_stack.rowSpacing + property string title: "Autostart" - property int delegateHeight: height*0.18 - SwitchDelegate { - id: ready_del - text: qsTr("say 'ready'") - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize - } - - checked: _cppAppSettings.loadSetting("ready_en") === "true" - width: parent.width - height: parent.delegateHeight - - font.pixelSize: options_stack.text_pixelSize - - onCheckedChanged: { - _cppAppSettings.writeSetting("ready_en",checked) - } - - indicator: SimpleIndicator{} + function updateSetting(key, val, del){ + speedBackend.writeSetting(key, val) } - ItemDelegate { - id: ready_delay_del - text: qsTr("delay (ms)") - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize + function loadSetting(key, del){ + var val + + val = speedBackend.readSetting(key) + + return val + } + + SmoothSwitchDelegate { + id: ready_del + + width: parent.width + height: options_stack.delegateHeight + + text: qsTr("say 'ready'") + + checked: parent.loadSetting("ready_en", ready_del) === "true" + + onCheckedChanged: { + parent.updateSetting("ready_en",checked, ready_del) } + } + + InputDelegate { + id: ready_delay_del + + width: parent.width + height: options_stack.delegateHeight enabled: ready_del.checked - width: parent.width - font.pixelSize: options_stack.text_pixelSize - height: parent.delegateHeight - TextField { - focus: true - placeholderText: qsTr("time") - width: parent.width * 0.3 - height: parent.height - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - inputMethodHints: Qt.ImhFormattedNumbersOnly + text: qsTr("delay (ms)") + inputHint: qsTr("time") + inputMethodHints: Qt.ImhFormattedNumbersOnly - text: _cppAppSettings.loadSetting("ready_delay") + inputText: autostart_col.loadSetting("ready_delay", ready_del) - onTextChanged: { - _cppAppSettings.writeSetting("ready_delay", text) - } + onInputFinished: { + autostart_col.updateSetting("ready_delay", inputText, ready_delay_del) } } - SwitchDelegate { + SmoothSwitchDelegate { id: at_marks_del - text: qsTr("say\n'at your marks'") - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize - } - checked: _cppAppSettings.loadSetting("at_marks_en") === "true" width: parent.width - //height: parent.delegateHeight * 1.5 + height: options_stack.delegateHeight - font.pixelSize: options_stack.text_pixelSize + text: qsTr("say 'at your marks'") + + checked: autostart_col.loadSetting("at_marks_en", ready_del) === "true" onCheckedChanged: { - _cppAppSettings.writeSetting("at_marks_en",at_marks_del.checked) + parent.updateSetting("at_marks_en",at_marks_del.checked, at_marks_del) } - - indicator: SimpleIndicator{} } - ItemDelegate { + InputDelegate { id: at_marks_delay_del + + width: parent.width + height: options_stack.delegateHeight + text: qsTr("delay (ms)") - contentItem: Text { - text: parent.text - color: StyleSettings.textColor - font.pixelSize: options_stack.text_pixelSize - } + inputHint: qsTr("time") + inputMethodHints: Qt.ImhFormattedNumbersOnly enabled: at_marks_del.checked + + inputText: autostart_col.loadSetting("at_marks_delay", at_marks_delay_del) + + onInputFinished: { + autostart_col.updateSetting("at_marks_delay", inputText, at_marks_delay_del) + } + } + } + } + + /*-----Page to connect to extenstions like a startpad or buzzer-----*/ + Component { + id: connect + Column { + id: connectCol + property string title: qsTr("Base Station") + + spacing: options_stack.rowSpacing + + property bool baseConnected: speedBackend.baseStationState === "connected" + + ConnectionDelegate { + id: connectToBaseDel + text: status.status === "connected" ? qsTr("disconnect"): status.status === "disconnected" ? qsTr("connect"):qsTr("connecting...") + + status: { "status": speedBackend.baseStationState } + connect: speedBackend.connectBaseStation + disconnect: speedBackend.disconnectBaseStation + type: "baseStation" + width: parent.width - height: parent.delegateHeight + height: options_stack.delegateHeight - font.pixelSize: options_stack.text_pixelSize + font.pixelSize: height * 0.6 + } - TextField { - focus: true - placeholderText: qsTr("time") - width: parent.width * 0.3 - height: parent.height - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - inputMethodHints: Qt.ImhFormattedNumbersOnly + InputDelegate { + id: baseStationIpDel - text: _cppAppSettings.loadSetting("at_marks_delay") + text: qsTr("IP-Adress") - onTextChanged: { - _cppAppSettings.writeSetting("at_marks_delay",text) + inputHint: "IP" + inputText: speedBackend.readSetting("baseStationIpAdress") + inputTextFieldWidth: width * 0.7 + + onInputTextChanged: { + speedBackend.writeSetting("baseStationIpAdress", inputText) + speedBackend.reloadBaseStationIpAdress() + } + + width: parent.width + height: !connectCol.baseConnected ? options_stack.delegateHeight:0 + + visible: height > 5 + + Behavior on height { + NumberAnimation { + duration: 400 + easing.type: Easing.Linear } } } + + SmoothSliderDelegate { + id: baseStationVolumeDel + text: qsTr("volume") + + property bool active: connectCol.baseConnected + + width: parent.width + height: active ? options_stack.delegateHeight:0 + + visible: height > 5 + + sliderValue: 0 + + onSliderFinished: { + speedBackend.writeSetting("soundVolume", sliderValue) + } + + onActiveChanged: { + + if(active){ + var val = speedBackend.readSetting("soundVolume") + console.log(val) + if(val !== "false"){ + sliderValue = parseFloat(val) + } + } + } + + Behavior on height { + NumberAnimation { + duration: 400 + easing.type: Easing.Linear + } + } + } + + NextPageDelegate { + id: baseStationConnectionsDel + text: qsTr("connected extensions") + + width: parent.width + height: connectCol.baseConnected ? options_stack.delegateHeight:0 + + visible: height > 5 + + onClicked: { + options_stack.push(baseStationConnections) + } + + Behavior on height { + NumberAnimation { + duration: 400 + easing.type: Easing.Linear + } + } + } + } + } + + /*-----Page to view devices that core connected to the pase startion-----*/ + Component{ + id: baseStationConnections + ListView { + id: baseStationConnections_list + + property string title: qsTr("connections") + + spacing: options_stack.rowSpacing + boundsBehavior: Flickable.StopAtBounds + + model: speedBackend.baseStationConnections.length + delegate: ConnectionDelegate { + + opacity: 1 + + width: parent.width + height: options_stack.delegateHeight + + text: speedBackend.baseStationConnections[index]["name"] + status: {'status': speedBackend.baseStationConnections[index]["state"], 'progress': speedBackend.baseStationConnections[index]["progress"]} + } } } @@ -421,18 +516,32 @@ Popup { property: "opacity" from: 0 to: 1 - duration: 200 + duration: 300 easing.type: Easing.InOutQuad } + + NumberAnimation { + property: "x" + from: width * 0.1 + to: 0 + duration: 300 + } } pushExit: Transition { NumberAnimation { property: "opacity" from: 1 to: 0 - duration: 200 + duration: 300 easing.type: Easing.InOutQuad } + + NumberAnimation { + property: "x" + to: -width * 0.1 + from: 0 + duration: 300 + } } popExit: Transition { @@ -440,18 +549,30 @@ Popup { property: "opacity" from: 1 to: 0 - duration: 200 + duration: 300 easing.type: Easing.InOutQuad } + NumberAnimation { + property: "x" + to: width * 0.1 + from: 0 + duration: 300 + } } popEnter: Transition { NumberAnimation { property: "opacity" from: 0 to: 1 - duration: 200 + duration: 300 easing.type: Easing.InOutQuad } + NumberAnimation { + property: "x" + from: -width * 0.1 + to: 0 + duration: 300 + } } } diff --git a/qml/components/ConnectionDelegate.qml b/qml/components/ConnectionDelegate.qml index c9cffa1..d0f749a 100644 --- a/qml/components/ConnectionDelegate.qml +++ b/qml/components/ConnectionDelegate.qml @@ -1,21 +1,29 @@ import QtQuick 2.0 import QtQuick.Controls 2.2 -ItemDelegate { +SmoothItemDelegate { id: control + property var status property var connect + property var disconnect + property string type text: qsTr(type) - enabled: status.status === "disconnected" - + enabled: (status.status === "disconnected" && control.connect !== undefined) || ( status.status === "connected" && control.disconnect !== undefined ) onClicked: { - connect(type) - if(status.status !== "connected"){ - statusIndicator.color_override = "red" - shortDelay.start() + + if(status.status === "disconnected"){ + connect() + if(status.status !== "connected"){ + statusIndicator.color_override = "red" + shortDelay.start() + } + } + else { + disconnect() } } @@ -33,9 +41,10 @@ ItemDelegate { id: statusItem anchors { right: parent.right + rightMargin: ( height / control.height / 2 ) * height verticalCenter: parent.verticalCenter } - height: parent.font.pixelSize + height: control.height * 0.4 width: height Rectangle { diff --git a/qml/components/ConnectionIcon.qml b/qml/components/ConnectionIcon.qml index 7566f45..1e37004 100644 --- a/qml/components/ConnectionIcon.qml +++ b/qml/components/ConnectionIcon.qml @@ -7,11 +7,12 @@ Image { source: "qrc:/graphics/icons/buzzer_black.png" mipmap: true - opacity: status !== "disconnected" ? 1:0 - visible: false + opacity: status === "connected" || status === "connecting" ? 1:0 + visible: true width: height onOpacityChanged: visible = true + SequentialAnimation { //rotating animation running: status === "connecting" @@ -29,12 +30,14 @@ Image { easing.type: Easing.InOutQuad } } + Behavior on rotation { NumberAnimation { duration: 200 easing.type: Easing.OutQuad } } + Behavior on opacity { NumberAnimation { duration: 200 diff --git a/qml/components/FadeAnimation.qml b/qml/components/FadeAnimation.qml index bb05356..40343b7 100644 --- a/qml/components/FadeAnimation.qml +++ b/qml/components/FadeAnimation.qml @@ -29,21 +29,42 @@ SequentialAnimation { property alias outEasingType: outAnimation.easing.type property alias inEasingType: inAnimation.easing.type property string easingType: "Quad" + ParallelAnimation { NumberAnimation { // in the default case, fade scale to 0 id: outAnimation target: root.target - property: root.fadeProperty + property: "scale" + duration: root.fadeDuration_in + to: 0.9 + easing.type: Easing["In"+root.easingType] + } + NumberAnimation { // in the default case, fade scale to 0 + id: outAnimation2 + target: root.target + property: "opacity" duration: root.fadeDuration_in to: 0 easing.type: Easing["In"+root.easingType] } - PropertyAction { } // actually change the property targeted by the Behavior between the 2 other animations - NumberAnimation { // in the default case, fade scale back to 1 - id: inAnimation - target: root.target - property: root.fadeProperty - duration: root.fadeDuration_out - to: 1 - easing.type: Easing["Out"+root.easingType] } + PropertyAction { } // actually change the property targeted by the Behavior between the 2 other animations + ParallelAnimation { + NumberAnimation { // in the default case, fade scale back to 1 + id: inAnimation + target: root.target + property: root.fadeProperty + duration: root.fadeDuration_out + to: 1 + easing.type: Easing["Out"+root.easingType] + } + NumberAnimation { // in the default case, fade scale to 0 + id: inAnimation2 + target: root.target + property: "opacity" + duration: root.fadeDuration_in + to: 1 + easing.type: Easing["In"+root.easingType] + } + } + } diff --git a/qml/components/FancyBusyIndicator.qml b/qml/components/FancyBusyIndicator.qml new file mode 100644 index 0000000..32d8421 --- /dev/null +++ b/qml/components/FancyBusyIndicator.qml @@ -0,0 +1,95 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.4 +import QtQuick.Controls.Styles 1.2 + +BusyIndicator { + id: control + + property double animationSpeed: 0.5 + + contentItem: Item { + implicitWidth: 64 + implicitHeight: 64 + + Item { + id: item + + x: parent.width / 2 - 32 + y: parent.height / 2 - 32 + + width: 64 + height: 64 + + opacity: control.running ? 1 : 0 + + property int currentHeight: 0 + + onCurrentHeightChanged: { + } + + Behavior on opacity { + OpacityAnimator { + duration: 250 + } + } + + SequentialAnimation { + loops: Animation.Infinite + + running: true + + NumberAnimation { + target: item + + duration: 2000 * 1/control.animationSpeed + + to: 1000 + + properties: "currentHeight" + + easing.type: Easing.InOutQuad + + } + + NumberAnimation { + target: item + + duration: 2000 * 1/control.animationSpeed + + to: 0 + + properties: "currentHeight" + + easing.type: Easing.InOutQuad + + } + } + + Row { + + anchors.fill: parent + + spacing: item.width / 9 + + Repeater { + id: repeater + model: 5 + + Rectangle { + + property double heightMultiplier: Math.abs( Math.sin(( (item.currentHeight + (index*20))*0.01) * (Math.PI/2) ) ) + + anchors.verticalCenter: parent.verticalCenter + + width: item.width / 9 + height: ( heightMultiplier ) * ( item.height - 1 ) + 1 + + radius: width * 0.5 + + color: "#21be2b" + } + } + } + } + } +} diff --git a/qml/components/FancyButton.qml b/qml/components/FancyButton.qml new file mode 100644 index 0000000..28dd8a3 --- /dev/null +++ b/qml/components/FancyButton.qml @@ -0,0 +1,76 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.4 +import QtGraphicalEffects 1.0 + +Button { + id: control + + property string image + property color backgroundColor: appTheme.style.buttonColor + property real imageScale: 1 + property double glowRadius: 0.001 + property double glowSpread: 0.2 + property bool glowVisible: true + property double glowScale: 0.75 + property double glowOpacity: Math.pow( control.opacity, 100 ) + + + + //scale: control.pressed ? 0.8:1 + + Behavior on scale { + PropertyAnimation { + duration: 100 + } + } + + background: Item { + id: controlBackgroundContainer + + RectangularGlow { + id: effect + glowRadius: control.glowRadius + spread: control.glowSpread + color: "black" + + visible: control.glowVisible + + cornerRadius: controlBackground.radius + anchors.fill: controlBackground + scale: control.glowScale + opacity: control.glowOpacity + } + + Rectangle { + id: controlBackground + + anchors.fill: parent + + radius: height * 0.5 + + color: control.down ? Qt.darker(control.backgroundColor, 1.2) : control.backgroundColor + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + + Image { + id: buttonIcon + source: control.image + + anchors.centerIn: parent + height: parent.height * 0.5 + width: height + + mipmap: true + + fillMode: Image.PreserveAspectFit + + scale: control.imageScale + } + } + } + +} diff --git a/qml/components/InputDelegate.qml b/qml/components/InputDelegate.qml new file mode 100644 index 0000000..306321b --- /dev/null +++ b/qml/components/InputDelegate.qml @@ -0,0 +1,67 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 + +SmoothItemDelegate { + id: control + + property string inputText: "" + property string inputHint: "" + + signal inputFinished() + + property int inputMethodHints: Qt.ImhNone + + property int inputTextFieldWidth: control.width * 0.3 + + onInputTextChanged: { + textField.text = inputText + } + + width: parent.width + rightPadding: textField.width + width * 0.02 + height: parent.delegateHeight + + TextField { + id: textField + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + width: control.inputTextFieldWidth + height: parent.height + + focus: true + placeholderText: control.inputHint + + font.pixelSize: height * 0.4 + + inputMethodHints: control.inputMethodHints + + palette.text: appTheme.style.textColor + + onTextChanged: { + control.inputText = text + control.inputTextChanged() + } + + onActiveFocusChanged: { + if(!activeFocus){ + control.inputFinished() + } + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 40 + color: "transparent"//control.enabled ? "transparent" : "#353637" + border.color: (textField.activeFocus ? "#21be2b" : "#999999") + radius: height * 0.3 + } + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } +} diff --git a/qml/components/NextPageDelegate.qml b/qml/components/NextPageDelegate.qml new file mode 100644 index 0000000..6c16109 --- /dev/null +++ b/qml/components/NextPageDelegate.qml @@ -0,0 +1,22 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.2 + +SmoothItemDelegate { + id: control + text: "" + + rightPadding: forwardImage.width * 1.1 + + Image { + id: forwardImage + source: appTheme.style.backIcon + rotation: 180 + height: control.height * 0.4 + width: height + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: control.width * 0.01 + } + } +} diff --git a/qml/components/ProgressCircle.qml b/qml/components/ProgressCircle.qml index 3fa6350..a1e7317 100644 --- a/qml/components/ProgressCircle.qml +++ b/qml/components/ProgressCircle.qml @@ -47,7 +47,7 @@ Item { enabled: true NumberAnimation { duration: root.animationDuration - easing.type: Easing.InOutCubic + easing.type: Easing.Linear } } @@ -56,7 +56,7 @@ Item { enabled: true NumberAnimation { duration: root.animationDuration - easing.type: Easing.InOutCubic + easing.type: Easing.Linear } } diff --git a/qml/components/RemoteDataListView.qml b/qml/components/RemoteDataListView.qml new file mode 100644 index 0000000..b750739 --- /dev/null +++ b/qml/components/RemoteDataListView.qml @@ -0,0 +1,86 @@ +import QtQuick 2.10 +import QtQuick.Controls 2.4 + +Item { + id: control + + property var loadData + property var listData: ({}) + property Component delegate + property alias view: listView + + property int status: -1 + + property alias contentY: listView.contentY + property alias model: listView.model + + signal refresh() + + Component.onCompleted: { + + } + + ListView { + id: listView + + model: control.listData.length + + anchors.fill: parent + + boundsBehavior: Flickable.DragOverBounds + boundsMovement: Flickable.StopAtBounds + + anchors.margins: 1 + anchors.rightMargin: 14 + clip: true + + //enabled: status === 200 || status === 902 + //opacity: enabled ? 1:0 + + ScrollBar.vertical: ScrollBar { + parent: listView.parent + + anchors { + top: listView.top + left: listView.right + margins: 10 + leftMargin: 3 + bottom: listView.bottom + } + + width: 8 + + visible: listView.model > 0 + + active: true + } + + delegate: control.delegate + + onContentYChanged: { +/* + if(contentY < -listView.height * 0.3 && control.status !== 905){ + contentY = 0 + control.refresh() + }*/ + } + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + } + + FancyBusyIndicator { + anchors.centerIn: parent + opacity: !(status === 200 || status === 902) ? 1:0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } +} diff --git a/qml/components/SmoothItemDelegate.qml b/qml/components/SmoothItemDelegate.qml new file mode 100644 index 0000000..c3627d0 --- /dev/null +++ b/qml/components/SmoothItemDelegate.qml @@ -0,0 +1,59 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.2 + +ItemDelegate { + id: control + text: "" + property color textColor: appTheme.style.textColor + property alias backgroundRect: backgroundRect + + opacity: enabled ? 1 : 0.2 + + contentItem: Text { + visible: false + } + + Text { + + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: control.width * 0.02 + right: parent.right + rightMargin: control.rightPadding + } + + text: control.text + color: appTheme.style.textColor + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + + fontSizeMode: Text.Fit + + font.pixelSize: control.height * 0.4 + + minimumPixelSize: 1 + } + + width: parent.width + + background: Rectangle { + id: backgroundRect + color: control.down ? appTheme.style.delegatePressedColor : appTheme.style.delegateBackgroundColor + + radius: height * 0.3 + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } +} diff --git a/qml/components/SmoothSliderDelegate.qml b/qml/components/SmoothSliderDelegate.qml new file mode 100644 index 0000000..b4815da --- /dev/null +++ b/qml/components/SmoothSliderDelegate.qml @@ -0,0 +1,71 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.4 + +SmoothItemDelegate { + id: control + + property double sliderValue: 0.5 + signal sliderFinished() + + onSliderValueChanged: { + slider.value = control.sliderValue + } + + rightPadding: slider.width + width * 0.02 + + Slider { + id: slider + + anchors { + right: parent.right + rightMargin: parent.width * 0.01 + verticalCenter: parent.verticalCenter + } + + height: control.height * 0.4 + width: parent.width * 0.6 + + onValueChanged: { + control.sliderValue = value + } + + onPressedChanged: { + if(!pressed){ + control.sliderFinished() + } + } + + background: Rectangle { + x: slider.leftPadding + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: 200 + implicitHeight: 4 + width: slider.availableWidth + height: slider.height * 0.2 + radius: height * 0.5 + color: "#bdbebf" + + Rectangle { + width: slider.visualPosition * parent.width + height: parent.height + color: "#21be2b" + radius: height * 0.5 + } + } + + handle: Rectangle { + x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + implicitWidth: 26 + implicitHeight: 26 + + width: slider.height + height: width + + radius: height * 0.5 + color: slider.pressed ? "#f0f0f0" : "#f6f6f6" + border.color: "#bdbebf" + } + } + +} diff --git a/qml/components/SmoothSwitchDelegate.qml b/qml/components/SmoothSwitchDelegate.qml new file mode 100644 index 0000000..8942d21 --- /dev/null +++ b/qml/components/SmoothSwitchDelegate.qml @@ -0,0 +1,87 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +SwitchDelegate { + id: control + + implicitHeight: 50 + implicitWidth: 100 + + baselineOffset: 100 + + contentItem: Text { + visible: false + } + + Text { + + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: indicator.left + leftMargin: control.width * 0.02 + rightMargin: control.width * 0.01 + } + + text: control.text + color: appTheme.style.textColor + + fontSizeMode: Text.Fit + + font.pixelSize: control.height * 0.4 + } + + indicator: Rectangle { + + property bool checked: parent.checked + property bool down: parent.down + + height: parent.height * 0.6 + width: height * 1.84 + + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: control.width * 0.02 + } + + radius: height * 0.5 + color: parent.checked ? "#17a81a" : "transparent" + border.color: parent.checked ? "#17a81a" : "#cccccc" + + Behavior on color{ + ColorAnimation{ + duration: 200 + } + } + + Rectangle { + x: parent.checked ? parent.width - width : 0 + width: parent.height + height: parent.height + radius: height * 0.5 + color: parent.down ? "#cccccc" : "#ffffff" + border.color: parent.checked ? (parent.down ? "#17a81a" : "#21be2b") : "#999999" + Behavior on x{ + NumberAnimation { + property: "x" + duration: 200 + easing.type: Easing.InOutQuad + } + } + } + } + + background: Rectangle { + opacity: enabled ? 1 : 0.3 + color: control.down ? appTheme.style.delegatePressedColor : appTheme.style.delegateBackgroundColor + + radius: height * 0.3 + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } +} diff --git a/qml/main.qml b/qml/main.qml index 09abe66..50b56da 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -19,13 +19,13 @@ import QtQuick 2.9 import QtMultimedia 5.8 import QtQuick.Window 2.2 import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 import "." import "./components" -import "./connections" -import "./styles" +import "./ProfilesDialog" //import QtQuick.Layouts 1.11 -import com.itsblue.speedclimbingstopwatch 1.0 +import com.itsblue.speedclimbingstopwatch 2.0 Window { visible: true @@ -35,173 +35,75 @@ Window { property date currentTime: new Date() property int millis: 0 - onBeforeRendering: { - StyleSettings.refreshTheme() - } - Page { id:root anchors.fill: parent - property double startTime: 0 - property double stoppedTime: 0 - property double currTime - - property double buzzer_offset - property double last_button_pressed - - property var last_run : { - 'stop_type': "", 'time': 0, 'react_time': 0 - }; - - //array that contains all connections an their atatus - property var connections: { - 'buzzer': buzzerConn.status, - 'startpad': startpadConn.status - } - //set default state to IDLE state: "IDLE" Rectangle { id: backgroundRect anchors.fill: parent - color: StyleSettings.backgroundColor - } + color: appTheme.style.backgroundColor - BuzzerConn { - id: buzzerConn - onPushed: { - // the buzzer was pushed - root.stop("buzzer") - } - } - - StartpadConn { - id: startpadConn - active: true - onActiveChanged: { - console.log("active changed: "+active) - } - - onPushed: { - active = false - console.log("startpad triggered") - var offset = _cppStartpadConn.get("offset") - var last_pressed = _cppStartpadConn.get("lastpressed") - var trigger_time = (last_pressed + offset) - root.last_run.react_time = trigger_time - root.startTime - if(trigger_time - root.startTime <= 0){ - root.stop("false") + Behavior on color { + ColorAnimation { + duration: 200 } } } - Timer { - //timer that updates the currTime variable - running: true - repeat: true - interval: 1 - onTriggered: { - root.currTime = new Date().getTime() + SpeedBackend { + id: speedBackend + + onStateChanged: { + var stateString + console.log("race state changed to: " + state) + switch (state){ + case 0: + stateString = "IDLE" + break; + case 1: + stateString = "STARTING" + settingsDialog.close() + break; + case 2: + stateString = "WAITING" + settingsDialog.close() + break; + case 3: + stateString = "RUNNING" + settingsDialog.close() + break; + case 4: + stateString = "STOPPED" + settingsDialog.close() + } + root.state = stateString } } - Timer { - id: next_actionTimer - - property string action - property double started_at - - running: false - repeat: false - onRunningChanged: { - if(!running){ - started_at = 0 - if(action == "NONE"){ - time.text = "0.000 sec" - } - return - } - - if(action === "at_marks"){ - started_at = new Date().getTime() - time.text = "at your\nmarks" - } - else if(action === "ready"){ - started_at = new Date().getTime() - time.text = "ready" - } - } - onTriggered: { - if(action === "at_marks"){ - at_marksSound.play() - } - else if(action === "ready"){ - action = "NONE" - readySound.play() - } - - } - } - - SoundEffect { - id: at_marksSound - source: "qrc:/sounds/at_marks_1.wav" - - onPlayingChanged: { - if(!playing && root.state==="STARTING"){ - if(_cppAppSettings.loadSetting("ready_en") === "true"){ - next_actionTimer.action = "ready" - next_actionTimer.interval = _cppAppSettings.loadSetting("ready_delay")>0 ? _cppAppSettings.loadSetting("ready_delay"):1 - next_actionTimer.start() - } - else{ - startSound.play() - } - } - } - } - - SoundEffect { - id: readySound - source: "qrc:/sounds/ready_1.wav" - onPlayingChanged: { - if(!playing && root.state==="STARTING"){ - - startSound.play() - } - } - } - - SoundEffect { - //start sound - id: startSound - source: "qrc:/sounds/OFFICAL_IFSC_STARTIGNAL.wav" - - onPlayingChanged: { - if(!playing && root.state==="STARTING"){ - - console.log(root.startTime) - _cppStartpadConn.appendCommand("SET_LED_RUNNING") - root.currTime = root.startTime - time.text = ( ( root.currTime - root.startTime ) / 1000 ).toFixed(3) + " sec" - root.state = "RUNNING" - } - else if(playing) { - console.log("start sound started") - root.startTime = _cppBuzzerConn.get("currtime") + 3100 //set the startime to be 0 after the starttone - startpadConn.active = true - } - } - + AppTheme { + id: appTheme } /*------------------------ Timer text an upper line ------------------------*/ + RectangularGlow { + id: effect_2 + glowRadius: 7 + spread: 0.02 + color: "black" + opacity: 0.18 + anchors.fill: topContainerItm + scale: 1 + } + Item { - id: time_container + id: topContainerItm + anchors { top: parent.top left: parent.left @@ -213,97 +115,238 @@ Window { Rectangle { anchors.fill: parent - color: StyleSettings.menuColor + color: appTheme.style.menuColor + + Behavior on color { + ColorAnimation { + duration: 200 + } + } } - //height: root.landscape() ? undefined:parent.height * 0.15 - Label { - id: time - text: qsTr("Click start to start") + Text { + id: topLa anchors.centerIn: parent - //font.pixelSize: root.landscape() ? parent.width * 0.1:parent.height * 0.3 - elide: "ElideRight" - color: StyleSettings.textColor + + opacity: ( speedBackend.state < 3 ) ? 1:0 + + width: parent.width * 0.7 + + text: qsTr("Click Start to start") + + color: appTheme.style.textColor + + fontSizeMode: Text.Fit + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + font.pixelSize: root.landscape() ? parent.width * 0.15 : parent.height * 0.4 + + minimumPixelSize: 1 + Behavior on text { - enabled: root.state !== "RUNNING" - FadeAnimation { - target: time + FadeAnimation{ + target: topLa + fadeDuration: 100 } } } - Label { - id: react_time - property int rtime: root.last_run.react_time - text: qsTr("reaction time (ms): ") + Math.round(rtime) - opacity: (root.state === "RUNNING" || root.sate === "STARTING" || root.state === "STOPPED") && rtime !== 0 ? 1:0 - color: StyleSettings.textColor + Column { + id: timerCol + + anchors.fill: parent + + opacity: ( speedBackend.state < 3 ) ? 0:1 + + spacing: height * 0.05 + + Repeater { + id: timerRep + + model: speedBackend.timers.length + + delegate: Item { + id: timerDel + + width: parent.width + height: timerRep.model > 1 ? ( timerCol.height * 0.9 ) / timerRep.model:timerCol.height + + Label { + id: timerTextLa + + anchors.centerIn: parent + + width: ( parent.width * 0.8 ) + height: parent.height + + elide: "ElideRight" + color: appTheme.style.textColor + + text: speedBackend.timers[index]["text"] + + fontSizeMode: Text.Fit + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + font.pixelSize: root.landscape() ? parent.width * 0.15 : parent.height * 0.4 + + minimumPixelSize: 1 + + Behavior on text { + enabled: root.state !== "RUNNING" + FadeAnimation { + target: timerTextLa + } + } + } + + Label { + id: react_time + + property int rtime: speedBackend.timers[index]["reacttime"] + + anchors { + centerIn: parent + verticalCenterOffset: parent.height * 0.25 + } + + width: ( parent.width * 0.6 ) + height: parent.height + + fontSizeMode: Text.Fit + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + text: qsTr("reaction time (ms): ") + Math.round(rtime) + + opacity: (root.state === "RUNNING" || root.state === "STOPPED") && rtime !== 0 ? 1:0 + + color: appTheme.style.textColor + + font.pixelSize: timerTextLa.font.pixelSize * 0.5 + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + } + + Item { + id: connectionIconContainer + + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: parent.bottom + bottomMargin: root.landscape() ? 0:parent.height * 0.8 + rightMargin: root.landscape() ? parent.width * 0.8:0 + } + + ConnectionIcon { + id: baseConnConnIcon + status: speedBackend.baseStationState + + source: appTheme.style.baseStationIcon anchors { - horizontalCenter: parent.horizontalCenter - top: time.bottom - topMargin: parent.height * 0.1 + top: parent.top + topMargin: 10 + left: parent.left + leftMargin: 10 } - Timer { - running: root.state === "RUNNING" || root.sate === "STARTING" || root.state === "STOPPED" - repeat: true - interval: 1 - onTriggered: { - react_time.rtime = root.last_run.react_time + scale: 1.3 + height: !root.landscape()? parent.height*0.3:parent.width*0.3 + } + + Row { + id: connectedExtensionsRow + + anchors { + top: parent.top + topMargin: 10 + left: baseConnConnIcon.right + leftMargin: 1 + } + + height: parent.height + width: parent.width + + Repeater { + id: connectedExtensionsRep + anchors.fill: parent + model: speedBackend.baseStationConnections.length + delegate: ConnectionIcon { + id: buzzerConnIcon + status: speedBackend.baseStationConnections[index]["state"] + + source: { + var source + switch(speedBackend.baseStationConnections[index]["type"]){ + case "STARTPAD": + source = appTheme.style.startpadIcon + break + case "TOPPAD": + source = appTheme.style.buzzerIcon + break + } + } + + scale: 0 + + height: !root.landscape()? parent.height*0.17:parent.width*0.17 + width: status === "disconnected" ? 0:height + + Component.onCompleted: { + scale = 1 + } + + Behavior on scale { + NumberAnimation { + duration: 200 + } + } + + Behavior on width { + NumberAnimation { + duration: 200 + } + } } } } - - } - - ConnectionIcon { - id: buzzerLogo - source: "qrc:/graphics/icons/buzzer_black.png" - status: root.connections["buzzer"].status - anchors { - top: parent.top - topMargin: 10 - left: parent.left - leftMargin: 10 - } - height: root.landscape()? root.height*0.1:root.width*0.1 - - Component.onCompleted: { - console.log(root.connections.buzzer) - } - } - - ConnectionIcon { - id: startpadLogo - source: "qrc:/graphics/icons/startpad_black.png" - status: root.connections["startpad"].status - anchors { - top: parent.top - topMargin: 10 - left: parent.left - leftMargin: 5 + buzzerLogo.width - } - height: root.landscape()? root.height*0.1:root.width*0.1 - - Component.onCompleted: { - console.log(root.connections.buzzer) - } } Rectangle { id: upper_line width: root.landscape() ? 1:parent.width height: root.landscape() ? parent.height:1 - color: StyleSettings.lineColor - anchors.left: root.landscape() ? time_container.right:parent.left - anchors.top: root.landscape() ? parent.top:time_container.bottom + color: appTheme.style.lineColor + anchors.left: root.landscape() ? topContainerItm.right:parent.left + anchors.top: root.landscape() ? parent.top:topContainerItm.bottom anchors.bottom: root.landscape() ? parent.bottom:undefined + visible: false } - /*---------------------- - Start / Stop / Reset button - ----------------------*/ - Button { + // ---------------------------------- + // -- Start / Stop / Reset button --- + // ---------------------------------- + FancyButton { id : startButt text: qsTr("start") @@ -317,24 +360,20 @@ Window { contentItem: Text { //make text disappear } - height: root.landscape() ? size > parent.height * 0.9 ? parent.height * 0.9:size : size - width: root.landscape() ? size : size > parent.width * 0.9 ? parent.width * 0.9:size + height: root.landscape() ? (size > parent.height * 0.9 ? parent.height * 0.9:size) : (size > parent.width * 0.9 ? parent.width * 0.9:size) + width: height - background: Rectangle { - color: parent.pressed ? StyleSettings.buttonPressedColor:StyleSettings.buttonColor - border.color: StyleSettings.buttonBorderColor - border.width: 1 - radius: width / 2 - Label { - id: startButt_text - text: startButt.text - anchors.centerIn: parent - font.pixelSize: parent.height * 0.16 - font.family: "Helvetica" - color: enabled ? StyleSettings.textColor:StyleSettings.disabledTextColor - } + Label { + id: startButt_text + text: startButt.text + anchors.centerIn: parent + font.pixelSize: parent.height * 0.16 + font.family: "Helvetica" + color: enabled ? appTheme.style.textColor:appTheme.style.disabledTextColor } + backgroundColor: appTheme.style.buttonColor + Behavior on text { //animate a text change enabled: true @@ -349,7 +388,7 @@ Window { root.start() break case "RUNNING": - root.stop("manual") + root.stop(0) break case "STOPPED": root.reset() @@ -361,31 +400,30 @@ Window { ProgressCircle { id: prog anchors.fill: startButt - opacity: next_actionTimer.started_at > 0 ? 1:0 + opacity: root.state === "STARTING" || root.state === "IDLE" ? 1:0 + + scale: startButt.scale + lineWidth: 5 arcBegin: 0 - arcEnd: 360 * (( next_actionTimer.interval - ( new Date().getTime() - next_actionTimer.started_at ) ) / next_actionTimer.interval) + arcEnd: 360 * speedBackend.nextStartActionDelayProgress colorCircle: "grey" - animationDuration: 0 - - Timer { - id: prog_refresh - running: parent.opacity === 1 - interval: 1 - repeat: true - onTriggered: { - prog.arcEnd = 360 * (( next_actionTimer.interval - ( new Date().getTime() - next_actionTimer.started_at ) ) / next_actionTimer.interval) + Behavior on opacity { + NumberAnimation { + duration: 200 } } + + animationDuration: speedBackend.mode === 1 ? 150:0 } /*---------------------- Cancel button ----------------------*/ - RoundButton { + FancyButton { id: cancelButt text: qsTr("cancel") @@ -403,7 +441,7 @@ Window { enabled: root.state === "STARTING" onClicked: { - root.stop("false") + root.stop(1) } Behavior on scale { @@ -411,20 +449,17 @@ Window { duration: 200 } } - background: Rectangle { - color: parent.pressed ? StyleSettings.buttonPressedColor:StyleSettings.buttonColor - border.color: StyleSettings.buttonBorderColor - border.width: 1 - radius: width / 2 - Label { - id: cancelButt_text - text: cancelButt.text - anchors.centerIn: parent - font.pixelSize: parent.height * 0.16 - font.family: "Helvetica" - color: StyleSettings.textColor - } + + Label { + id: cancelButt_text + text: cancelButt.text + anchors.centerIn: parent + font.pixelSize: parent.height * 0.16 + font.family: "Helvetica" + color: appTheme.style.textColor } + + backgroundColor: appTheme.style.buttonColor } /*------ @@ -441,28 +476,54 @@ Window { case "startpad": startpadConn.connect() break + case "baseStation": + baseConn.connectToHost() + break } } } - // ProfilesDialog { - // id: profilesDialog - // } + ProfilesDialog { + id: profilesDialog + + property int margin: root.landscape() ? root.height * 0.05:root.width * 0.05 + + x: root.landscape() ? topContainerItm.width + margin:topContainerItm.x + margin + y: !root.landscape() ? topContainerItm.height + margin:topContainerItm.x + margin + width: root.landscape() ? root.width - topContainerItm.width - menu_container.width - margin * 2 : root.width - margin * 2 + height: !root.landscape() ? root.height - topContainerItm.height - menu_container.height - margin * 2 : root.height - margin * 2 + + + } /*------------------- lower line and menu -------------------*/ + Rectangle { + id: lowerLine width: root.landscape() ? 1:parent.width height: root.landscape() ? parent.height:1 - color: StyleSettings.lineColor + color: appTheme.style.lineColor anchors.right: root.landscape() ? menu_container.left:parent.right anchors.bottom: root.landscape() ? parent.bottom:menu_container.top anchors.top: root.landscape() ? parent.top:undefined + visible: false + } + + RectangularGlow { + id: effect + glowRadius: 7 + spread: 0.02 + color: "black" + opacity: 0.18 + anchors.fill: menu_container + scale: 1 } Item { id: menu_container + anchors { bottom: parent.bottom right: parent.right @@ -473,99 +534,114 @@ Window { } Rectangle { + id: lowerMenuBackground anchors.fill: parent - color: StyleSettings.menuColor - } + color: appTheme.style.menuColor - RoundButton { - id: settingsButt - - anchors { - //center - verticalCenter: root.landscape() ? undefined:parent.verticalCenter - horizontalCenter: root.landscape() ? parent.horizontalCenter:undefined - //set anchors - left: root.landscape() ? undefined:parent.left - top: root.landscape() ? parent.top:undefined - //align in landscape mode - //for two buttons: topMargin: root.landscape() ? (parent.height - (height * 2)) / 3:undefined - topMargin: root.landscape() ? (parent.height * 0.5 - (height * 0.5)):undefined - //align in portrait mode - //for two buttons: leftMargin: root.landscape() ? undefined:(parent.width - width * 2) / 3 - leftMargin: root.landscape() ? undefined:(parent.width * 0.5 - width * 0.5) - } - - height: root.landscape() ? parent.width * 0.7:parent.height * 0.7 - width: height - - onClicked: { - settingsDialog.open() - } - - background: Rectangle { - color: parent.pressed ? StyleSettings.buttonPressedColor:StyleSettings.buttonColor - border.color: StyleSettings.buttonBorderColor - border.width: 1 - radius: width / 2 - - Image { - id: settungsButt_Image - source: StyleSettings.settIcon - anchors.centerIn: parent - height: parent.height * 0.7 - width: parent.width * 0.7 - mipmap: true + Behavior on color { + ColorAnimation { + duration: 200 } } } - /* - RoundButton { - id: profilesButt + Grid { + id: loweMenuGrd - anchors { - verticalCenter: root.landscape() ? undefined:parent.verticalCenter - horizontalCenter: root.landscape() ? parent.horizontalCenter:undefined - left: root.landscape() ? undefined:settingsButt.right - top: root.landscape() ? settingsButt.bottom:undefined - topMargin: root.landscape() ? (parent.height - (height * 2)) / 3:undefined - leftMargin: root.landscape() ? undefined:(parent.width - width * 2) / 3 + property int spacingMultiplier: 200 * (getActiveChildren() - 1) + property int activeChildren: getActiveChildren() + + function getActiveChildren() { + var childrenCount = 0 + for (var i = 0; i < children.length; i++) + { + if(children[i].enabled){ + childrenCount ++ + } + } + + return childrenCount } - height: root.landscape() ? parent.width * 0.7:parent.height * 0.7 - width: height + anchors.centerIn: parent - onPressedChanged: { - if(pressed){ - background.color = "lightgrey" - } - else { - background.color = "white" + height: childrenRect.height + width: childrenRect.width + + rows: root.landscape() ? activeChildren:1 + columns: root.landscape() ? 1:activeChildren + + spacing: 0// root.landscape() ? parent.height * spacingMultiplier * 0.001:parent.width * spacingMultiplier * 0.001 + + Behavior on spacingMultiplier { + NumberAnimation { + duration: 200 } } - onClicked: { - profilesDialog.open() + FancyButton { + id: settingsButt + + height: root.landscape() ? menu_container.width * 0.7:menu_container.height * 0.7 + width: height + + onClicked: { + settingsDialog.open() + } + + image: appTheme.style.settIcon + + backgroundColor: parent.pressed ? appTheme.style.buttonPressedColor:appTheme.style.buttonColor + } - background: Rectangle { - color: "white" - border.color: "grey" - border.width: 1 - radius: width / 2 + Item { + height: profilesButt.height + width: profilesButt.height + } - Image { - id: profilesButt_Image - source: "qrc:/graphics/icons/user.png" - anchors.centerIn: parent - height: parent.height * 0.5 - width: parent.width * 0.5 - mipmap: true + FancyButton { + id: profilesButt + + enabled: height > 0 + + state: speedBackend.baseStationState === "connected" ? "visible":"hidden" + width: height + + onClicked: { + profilesDialog.open() } + + image: appTheme.style.profilesIcon + + backgroundColor: parent.pressed ? appTheme.style.buttonPressedColor:appTheme.style.buttonColor + + states: [ + State { + name: "hidden" + PropertyChanges { + target: profilesButt + height: 0 + } + }, + State { + name: "visible" + PropertyChanges { + target: profilesButt + height: root.landscape() ? menu_container.width * 0.7:menu_container.height * 0.7 + } + } + ] + + transitions: [ + Transition { + NumberAnimation { + properties: "height" + } + } + ] } } - */ - } /*---------------------- @@ -575,13 +651,11 @@ Window { State { name: "IDLE" //state for the start page - PropertyChanges { target: time; text: qsTr("Click start to start"); font.pixelSize: root.landscape() ? parent.width * 0.1:parent.height * 0.3; scale: 1 } PropertyChanges { - target: time_container; + target: topContainerItm; anchors.bottomMargin: root.landscape() ? undefined:parent.height * 0.1; anchors.rightMargin: root.landscape() ? parent.height * 0.05:0 } - PropertyChanges { target: startButt; enabled: true; text: qsTr("start"); @@ -589,8 +663,27 @@ Window { anchors.bottomMargin: parent.height * 0.5 - startButt.height * 0.5 anchors.rightMargin: parent.width * 0.5 - startButt.width * 0.5 } + PropertyChanges { + target: topLa + text: qsTr("Click Start to start") + } }, + State { + name: "WAITING" + //state when a false start occured and waiting for time calculation + PropertyChanges { + target: startButt; enabled: false; text: qsTr("waiting..."); + anchors.rightMargin: root.landscape() ? parent.width * 0.05:parent.width * 0.5 - startButt.width * 0.5 //put the button more to the right to hide the menu (only in landscape mode) + anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode) + } + PropertyChanges { target: cancelButt; scale: 0; enabled: false} + PropertyChanges { target: menu_container; } + PropertyChanges { + target: topLa + text: qsTr("please wait...") + } + }, State { name: "STARTING" //state for the start sequence @@ -598,31 +691,31 @@ Window { anchors.rightMargin: root.landscape() ? parent.width * 0.05:parent.width * 0.5 - startButt.width * 0.5 //put the button more to the right to hide the menu (only in landscape mode) anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode) } - PropertyChanges { target: time; text: "0.000 sec"; font.pixelSize: root.landscape() ? parent.width * 0.2:parent.height * 0.3; scale: 1 } PropertyChanges { target: cancelButt; scale: 1} PropertyChanges { target: menu_container; } + PropertyChanges { + target: topLa + text: qsTr("starting...") + } + }, State { name: "RUNNING" //state when the timer is running - PropertyChanges { target: time; text: Math.abs( ( ( root.currTime - root.startTime ) / 1000 ) ).toFixed(3) + " sec"; font.pixelSize: root.landscape() ? parent.width * 0.2:parent.height * 0.3; scale: 1 } PropertyChanges { target: startButt; enabled: true; text: "stop" anchors.rightMargin: root.landscape() ? parent.width * 0.05:parent.width * 0.5 - startButt.width * 0.5 //put the button more to the right to hide the menu (only in landscape mode) anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode) } - + PropertyChanges { + target: topLa + text: "" + } }, State { name: "STOPPED" //state when the meassuring is over - PropertyChanges { - target: time; - text: root.stoppedTime > 0 ? ( root.stoppedTime / 1000 ).toFixed(3) + " sec":qsTr("false start"); - font.pixelSize: root.landscape() ? parent.width * 0.15:parent.height * 0.1; - scale: 1 - } PropertyChanges { target: startButt; enabled: true; text: qsTr("reset"); @@ -631,10 +724,14 @@ Window { anchors.rightMargin: root.landscape() ? parent.height * 0.2 - startButt.height * 0.5:parent.width * 0.5 - startButt.width * 0.5 } PropertyChanges { - target: time_container; + target: topContainerItm; anchors.rightMargin: root.landscape() ? 0-startButt.width/2:undefined anchors.bottomMargin: root.landscape() ? undefined:0-startButt.height/2 } + PropertyChanges { + target: topLa + text: "" + } } ] @@ -643,20 +740,26 @@ Window { ----------------------*/ transitions: [ Transition { - NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } + NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } }, Transition { to: "STOPPED" - NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } + NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } }, Transition { to: "IDLE" - NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } + NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize,pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } }, Transition { + from: "STARTING" to: "RUNNING" //disable transitions for the RUNNING state + }, + Transition { + from: "RUNNING" + to: "WAITING" + //disable transitions for the RUNNING state } ] @@ -669,58 +772,28 @@ Window { /*----Functions to control the stopwatch----*/ function start(){ - root.state = "STARTING" - if(_cppAppSettings.loadSetting("at_marks_en") === "true"){ - next_actionTimer.action = "at_marks" - next_actionTimer.interval = _cppAppSettings.loadSetting("at_marks_delay")>0 ? _cppAppSettings.loadSetting("at_marks_delay"):1 - next_actionTimer.start() - return + var ret = speedBackend.startRace() + if(ret !== 200){ + console.log("+ --- error starting race: "+ret) } - if(_cppAppSettings.loadSetting("ready_en") === "true"){ - next_actionTimer.action = "ready" - next_actionTimer.interval = _cppAppSettings.loadSetting("ready_delay")>0 ? _cppAppSettings.loadSetting("ready_delay"):1 - next_actionTimer.start() - return - } - - startSound.play() } function stop(type){ - _cppStartpadConn.appendCommand("SET_LED_STARTING"); - switch(type){ - case "buzzer": - //the buzzer was pushed - root.buzzer_offset = _cppBuzzerConn.get("offset") - root.last_button_pressed = _cppBuzzerConn.get("lastpressed") - root.stoppedTime = (root.last_button_pressed + root.buzzer_offset) - root.startTime - root.state = "STOPPED" - //time.text = ( root.stoppedTime / 1000 ).toFixed(3) + " sec" - console.log("STOPPED: "+root.stoppedTime) - break - case "manual": - //the stop button was pressed - root.stoppedTime = new Date().getTime() - root.startTime - time.text = ( root.stoppedTime / 1000 ).toFixed(3) + " sec" - root.state = "STOPPED" - break - case "false": - //the cancel button was pressed - root.stoppedTime = 0 - time.text = "false start" - root.state = "STOPPED" - next_actionTimer.stop() - at_marksSound.stop() - readySound.stop() - startSound.stop() - break + + var ret = speedBackend.stopRace(type) + + if(ret !== 200){ + console.log("+ --- error stopping race: "+ret) } - startpadConn.active = true } function reset(){ - root.state = "IDLE" - root.last_run.react_time = 0 + + var ret = speedBackend.resetRace() + + if(ret !== 200){ + console.log("+ --- error resetting race: "+ret) + } } } } diff --git a/qml/qml.qrc b/qml/qml.qrc index 2faba7a..db55f87 100644 --- a/qml/qml.qrc +++ b/qml/qml.qrc @@ -1,19 +1,24 @@ main.qml - ProfilesDialog.qml SettingsDialog.qml components/ProgressCircle.qml - components/SimpleIndicator.qml components/ConnectionDelegate.qml components/FadeAnimation.qml - connections/BuzzerConn.qml - connections/StartpadConn.qml - styles/StyleSettings.qml - styles/qmldir - styles/Dark.js - styles/Light.js - styles/Default.js components/ConnectionIcon.qml + components/NextPageDelegate.qml + ErrorDialog.qml + components/FancyButton.qml + components/SmoothItemDelegate.qml + components/SmoothSwitchDelegate.qml + components/InputDelegate.qml + components/SmoothSliderDelegate.qml + components/RemoteDataListView.qml + components/FancyBusyIndicator.qml + ProfilesDialog/ProfilesDialog.qml + ProfilesDialog/ProfilesStack.qml + ProfilesDialog/ProfileListPage.qml + ProfilesDialog/AddProfilePage.qml + ProfilesDialog/ResultListPage.qml diff --git a/shared.qrc b/shared.qrc index 4f8acc8..c430b9e 100644 --- a/shared.qrc +++ b/shared.qrc @@ -15,10 +15,17 @@ sounds/at_marks_2.wav graphics/icons/buzzer_black.png graphics/icons/ok_black.png - translations/german.ts translations/de_DE.qm translations/de_DE.ts graphics/icons/settings_black.png graphics/icons/startpad_black.png + sounds/false.wav + graphics/icons/error.png + graphics/icons/BaseStation.png + graphics/icons/BaseStation_black.png + graphics/icons/buzzer.png + graphics/icons/startpad.png + graphics/icons/user_black.png + graphics/icons/ok.png diff --git a/sounds/false.wav b/sounds/false.wav new file mode 100644 index 0000000..ae4daa7 Binary files /dev/null and b/sounds/false.wav differ diff --git a/sources/appsettings.cpp b/sources/appsettings.cpp index 66bda66..ec0c601 100644 --- a/sources/appsettings.cpp +++ b/sources/appsettings.cpp @@ -17,6 +17,8 @@ #include "headers/appsettings.h" +AppSettings * pGlobalAppSettings = nullptr; + AppSettings::AppSettings(QObject* parent) :QObject(parent) { @@ -29,7 +31,11 @@ AppSettings::AppSettings(QObject* parent) this->setDefaultSetting("at_marks_en", "false"); this->setDefaultSetting("at_marks_delay", 0); - this->setDefaultSetting("theme", "Default"); + this->setDefaultSetting("theme", "Light"); + + this->setDefaultSetting("baseStationIpAdress", "192.168.4.1"); + + pGlobalAppSettings = this; } QString AppSettings::loadSetting(const QString &key) diff --git a/sources/apptheme.cpp b/sources/apptheme.cpp new file mode 100644 index 0000000..799f9d4 --- /dev/null +++ b/sources/apptheme.cpp @@ -0,0 +1,132 @@ +#include "headers/apptheme.h" + +AppTheme::AppTheme(QObject *parent) : QObject(parent) +{ + + QVariantMap tmpDarkTheme = { + {"backgroundColor", "#2d3037"}, + + {"buttonColor", "#202227"}, + {"buttonPressedColor", "#6ccaf2"}, + {"buttonBorderColor", "grey"}, + {"disabledButtonColor", "#555555"}, + + {"viewColor", "#202227"}, + {"menuColor", "#292b32"}, + + {"delegate1Color", "#202227"}, + {"delegate2Color", "#202227"}, + {"delegateBackgroundColor", "#202227"}, + {"delegatePressedColor", "#41454f"}, + + {"textColor", "#ffffff"}, + {"textDarkColor", "#232323"}, + {"disabledTextColor", "#777777"}, + + {"sliderColor", "#6ccaf2"}, + + {"errorColor", "#ba3f62"}, + {"infoColor", "#3fba62"}, + + {"lineColor", "grey"}, + + {"backIcon", "qrc:/graphics/icons/back.png"}, + {"settIcon", "qrc:/graphics/icons/settings.png"}, + {"buzzerIcon", "qrc:/graphics/icons/buzzer.png"}, + {"startpadIcon", "qrc:/graphics/icons/startpad.png"}, + {"baseStationIcon", "qrc:/graphics/icons/BaseStation.png"}, + {"profilesIcon", "qrc:/graphics/icons/user.png"}, + {"confirmIcon", "qrc:/graphics/icons/ok.png"} + + }; + this->darkTheme = tmpDarkTheme; + + QVariantMap tmpLightTheme = { + {"backgroundColor", "white"}, + + {"buttonColor", "white"}, + {"buttonPressedColor", "lightgrey"}, + {"buttonBorderColor", "grey"}, + {"disabledButtonColor", "#d5d5d5"}, + + {"viewColor", "white"}, + {"menuColor", "#f8f8f8"}, + + {"delegate1Color", "#202227"}, + {"delegate2Color", "#202227"}, + {"delegateBackgroundColor", "white"}, + {"delegatePressedColor", "#dddedf"}, + + {"textColor", "black"}, + {"textDarkColor", "#232323"}, + {"disabledTextColor", "grey"}, + + {"sliderColor", "#6ccaf2"}, + + {"errorColor", "#ba3f62"}, + {"infoColor", "#3fba62"}, + + {"lineColor", "grey"}, + + {"backIcon", "qrc:/graphics/icons/back_black.png"}, + {"settIcon", "qrc:/graphics/icons/settings_black.png"}, + {"buzzerIcon", "qrc:/graphics/icons/buzzer_black.png"}, + {"startpadIcon", "qrc:/graphics/icons/startpad_black.png"}, + {"baseStationIcon", "qrc:/graphics/icons/BaseStation_black.png"}, + {"profilesIcon", "qrc:/graphics/icons/user_black.png"}, + {"confirmIcon", "qrc:/graphics/icons/ok_black.png"} + + }; + this->lightTheme = tmpLightTheme; + + QString currentThemeString = pGlobalAppSettings->loadSetting("theme"); + + if(currentThemeString == "Light"){ + this->currentTheme = &this->lightTheme; + } + else if (currentThemeString == "Dark") { + this->currentTheme = &this->darkTheme; + } + else { + this->currentTheme = &this->lightTheme; + } +} + +QVariant AppTheme::getStyle() { + return *this->currentTheme; +} + +void AppTheme::changeTheme() { + QString currentThemeString = pGlobalAppSettings->loadSetting("theme"); + QString newThemeString = "Light"; + + if(currentThemeString == "Light"){ + this->currentTheme = &this->darkTheme; + newThemeString = "Dark"; + + } + else if (currentThemeString == "Dark") { + this->currentTheme = &this->lightTheme; + newThemeString = "Light"; + } + else { + this->currentTheme = &this->lightTheme; + } + + pGlobalAppSettings->writeSetting("theme", newThemeString); + + emit this->styleChanged(); +} + +void AppTheme::refreshTheme() { + QString currentThemeString = pGlobalAppSettings->loadSetting("theme"); + + if(currentThemeString == "Light"){ + this->currentTheme = &this->lightTheme; + } + else if (currentThemeString == "Dark") { + this->currentTheme = &this->darkTheme; + } + + emit this->styleChanged(); +} diff --git a/sources/baseconn.cpp b/sources/baseconn.cpp new file mode 100644 index 0000000..7dd55d2 --- /dev/null +++ b/sources/baseconn.cpp @@ -0,0 +1,373 @@ +#include "headers/baseconn.h" + +BaseConn * pGlobalBaseConn = nullptr; + +BaseConn::BaseConn(QObject *parent) : QObject(parent) +{ + pGlobalBaseConn = this; + socket = new QTcpSocket(); + this->setState("disconnected"); + + connect(this->socket, SIGNAL(error(QAbstractSocket::SocketError)), + this, SLOT(gotError(QAbstractSocket::SocketError))); + + connect(this->socket, &QAbstractSocket::stateChanged, this, &BaseConn::socketStateChanged); + + this->nextConnectionId = 1; + this->connections = QVariantList({}); +} + +bool BaseConn::connectToHost() { + qDebug() << "connecting"; + setState("connecting"); + this->connection_progress = 0; + QEventLoop loop; + QTimer timer; + + timer.setSingleShot(true); + // quit the loop when the timer times out + loop.connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + //quit the loop when the connection was established + loop.connect(this->socket, SIGNAL(connected()), &loop, SLOT(quit())); + // start the timer before starting to connect + timer.start(3000); + //connect + this->socket->connectToHost(this->ip, this->port); + + //wait for the connection to finish (programm gets stuck in here) + loop.exec(); + + //loop finish + + if(timer.remainingTime() == -1){ + //the time has been triggered -> timeout + this->socket->abort(); + return(false); + } + + // stop the timer as the connection has been established + timer.stop(); + connect(this->socket, &QTcpSocket::readyRead, this, &BaseConn::readyRead); + this->connection_progress = 50; + + if(!this->init()){ + this->closeConnection(); + return false; + } + + this->setState("connected"); + + return(true); +} + +bool BaseConn::init() { + return true; +} + +void BaseConn::deInit() { + this->connections.clear(); +} + +void BaseConn::closeConnection() +{ + this->connections = QVariantList({}); + emit this->connectionsChanged(); + + qDebug() << "closing connection"; + switch (socket->state()) + { + case 0: + socket->disconnectFromHost(); + break; + case 2: + socket->abort(); + break; + default: + socket->abort(); + } + + this->deInit(); + setState("disconnected"); + // for(int i = 0; i < this->waitingRequests.length(); i++){ + // this->waitingRequests[i].reply = "ERR_NOT_CONNECTED"; + // this->waitingRequests[i].loop->quit(); + // return; + // } +} + +void BaseConn::gotError(QAbstractSocket::SocketError err) +{ + //qDebug() << "got error"; + QString strError = "unknown"; + switch (err) + { + case 0: + strError = "Connection was refused"; + break; + case 1: + strError = "Remote host closed the connection"; + this->closeConnection(); + break; + case 2: + strError = "Host address was not found"; + break; + case 5: + strError = "Connection timed out"; + break; + default: + strError = "Unknown error"; + } + + emit gotError(strError); + qDebug() << "got socket error: " << strError; +} + +// ------------------------------------- +// --- socket communication handling --- +// ------------------------------------- + +QVariantMap BaseConn::sendCommand(int header, QJsonValue data){ + if(this->state != "connected"){ + return {{"status", 910}, {"data", "not connected"}}; + } + + // generate id and witing requests entry + int thisId = nextConnectionId; + //qDebug() << "sending command: " << header << " with data: " << data << " and id: " << thisId; + nextConnectionId ++; + + QEventLoop loop; + QJsonObject reply; + + this->waitingRequests.append({thisId, &loop, reply}); + + QJsonObject requestObj; + requestObj.insert("id", thisId); + requestObj.insert("header", header); + requestObj.insert("data", data); + + + QString jsonRequest = QJsonDocument(requestObj).toJson(); + + QTimer timer; + + timer.setSingleShot(true); + // quit the loop when the timer times out + loop.connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + // quit the loop when the connection was established + // loop.connect(this, &BaseConn::gotReply, &loop, &QEventLoop::quit); + // start the timer before starting to connect + timer.start(3000); + + //write data + + socket->write(jsonRequest.toLatin1()); + + //wait for an answer to finish (programm gets stuck in here) + loop.exec(); + + //loop finished + if(timer.remainingTime() == -1){ + //the time has been triggered -> timeout + + return {{"status", 911}, {"data", ""}}; + } + + for(int i = 0; iwaitingRequests.length(); i++){ + if(this->waitingRequests[i].id == thisId){ + reply = this->waitingRequests[i].reply; + } + } + + // stop the timer as the connection has been established + timer.stop(); + + return {{"status", reply.value("header").toInt()}, {"data", reply.value("data").toVariant()}}; + +} + +void BaseConn::readyRead() { + + //qDebug() << "ready to ready " << socket->bytesAvailable() << " bytes" ; + QString reply = socket->readAll(); + + //qWarning() << "socket read: " << reply; + + processSocketMessage(reply); +} + +void BaseConn::processSocketMessage(QString message){ + QString startKey = ""; + QString endKey = ""; + + //qWarning() << "... processing message now ... : " << message; + + if(message == ""){ + return; + } + + if((message.startsWith(startKey) && message.endsWith(endKey)) && (message.count(startKey) == 1 && message.count(endKey) == 1)){ + // non-split message ( e.g.: 123456789 + } + else if(!message.contains(endKey) && (!this->readBuffer.isEmpty() || message.startsWith(startKey))){ + // begin of a split message ( e.g.: 123 ) + // or middle of a split message ( e.g.: 456 ) + //qWarning() << "this is a begin or middle of split a message"; + this->readBuffer += message; + return; + } + else if(!message.contains(startKey) && message.endsWith(endKey)) { + // end of a split message ( e.g.: 789 ) + + if(!this->readBuffer.isEmpty()){ + message = readBuffer + message; + readBuffer.clear(); + } + } + else if((message.count(startKey) > 1 || message.count(endKey) > 1) || (message.contains(endKey) && !message.endsWith(endKey) && message.contains(startKey) && !message.startsWith(startKey))) { + // multiple messages in one packet ( e.g.: 123456789987654321 ) + // or multiple message fragments in one message ( e.g.: 56789987654321 or 5678998765 ) + //qDebug() << "detected multiple messages"; + + int startOfSecondMessage = message.lastIndexOf(startKey); + // process first part of message + QString firstMessage = message.left(startOfSecondMessage); + this->processSocketMessage(firstMessage); + // process second part of message + QString secondMessage = message.right(message.length() - startOfSecondMessage); + this->processSocketMessage(secondMessage); + + return; + } + else { + // invalid message + return; + } + + //qWarning() << "... done processing, message: " << message; + this->socketReplyRecieved(message); +} + +void BaseConn::socketReplyRecieved(QString reply) { + reply.replace("", ""); + reply.replace("", ""); + + int id = 0; + + QJsonDocument jsonReply = QJsonDocument::fromJson(reply.toUtf8()); + QJsonObject replyObj = jsonReply.object(); + + //qDebug() << "got: " << reply; + + if(!replyObj.isEmpty()){ + id = replyObj.value("id").toInt(); + + for(int i = 0; i < this->waitingRequests.length(); i++){ + if(this->waitingRequests[i].id == id){ + this->waitingRequests[i].reply = replyObj; + if(this->waitingRequests[i].loop != nullptr){ + this->waitingRequests[i].loop->quit(); + } + return; + } + } + } + + latestReadReply = reply; + emit gotUnexpectedReply(reply); +} + +// ------------------------ +// --- helper functions --- +// ------------------------ + +int BaseConn::writeRemoteSetting(QString key, QString value) { + QJsonArray requestData; + requestData.append(key); + requestData.append(value); + return this->sendCommand(3000, requestData)["status"].toInt(); +} + +void BaseConn::setIP(const QString &ipAdress){ + this->ip = ipAdress; +} + +QString BaseConn::getIP() const +{ + return(this->ip); +} + +QString BaseConn::getState() const +{ + return(this->state); +} + +void BaseConn::setState(QString newState){ + this->state = newState; + emit stateChanged(); +} + +void BaseConn::socketStateChanged(QAbstractSocket::SocketState socketState) { + switch (socketState) { + case QAbstractSocket::UnconnectedState: + { + this->deInit(); + this->setState("disconnected"); + break; + } + case QAbstractSocket::ConnectedState: + { + //this->setState("connected"); + break; + } + default: + { + qDebug() << "+ --- UNKNOWN SOCKET STATE: " << socketState; + break; + } + } +} + +int BaseConn::getProgress() const +{ + return(connection_progress); +} + +bool BaseConn::refreshConnections() { + QVariantMap reply = this->sendCommand(2006); + + if(reply["status"] != 200){ + //handle Error!! + if(reply["status"] == 910){ + this->connections = QVariantList({}); + return true; + } + qDebug() << "+ --- error refreshing connections: " << reply["status"]; + return false; + } + + QVariantList tmpConnections = reply["data"].toList(); + + if(this->connections != reply["data"].toList()){ + this->connections = reply["data"].toList(); + emit this->connectionsChanged(); + } + + return true; + +} + +QVariant BaseConn::getConnections() { + return(connections); + /* + "id": "id of the extention (int)", + "type": "type of the extention (can be: 'STARTPAD', 'TOPPAD')", + "name": "name of the extention", + "ip": "ip-adress of he extention (string)", + "state": "state of the extention (can be: 'disconnected', 'connecting', 'connected')" + */ + //QVariantMap conn = {{"id",0}, {"type","STARTPAD"}, {"name", "startpad1"}, {"ip", "192.168.4.11"}, {"state", "connected"}}; + //QVariantMap conn1 = {{"id",0}, {"type","TOPPAD"}, {"name", "buzzer1"}, {"ip", "192.168.4.10"}, {"state", "connected"}}; + //QVariantList conns = {conn, conn1}; + //return conns; +} diff --git a/sources/climbingrace.cpp b/sources/climbingrace.cpp new file mode 100644 index 0000000..28d0f8b --- /dev/null +++ b/sources/climbingrace.cpp @@ -0,0 +1,637 @@ +#include "headers/climbingrace.h" + +/* + * manages: + * - global state + * - timers + * - sounds + * - next start action + * - next start action delay progress + * - settings (remote and local) + */ + +ClimbingRace::ClimbingRace(QObject *parent) : QObject(parent) +{ + this->state = IDLE; + this->mode = LOCAL; + + this->appSettings = new AppSettings; + this->baseConn = new BaseConn; + + this->baseConn->setIP(pGlobalAppSettings->loadSetting("baseStationIpAdress")); + connect(this->baseConn, &BaseConn::stateChanged, this, &ClimbingRace::baseStationStateChanged); + connect(this->baseConn, &BaseConn::connectionsChanged, this, &ClimbingRace::baseStationConnectionsChanged); + + this->speedTimers.append( new SpeedTimer ); + + this->player = new QMediaPlayer; + this->date = new QDateTime; + + this->nextStartActionTimer = new QTimer(this); + nextStartActionTimer->setSingleShot(true); + + this->baseStationSyncTimer = new QTimer(); + this->baseStationSyncTimer->setInterval(100); + this->baseStationSyncTimer->setSingleShot(true); + this->baseStationSyncTimer->connect(this->baseStationSyncTimer, &QTimer::timeout, this, &ClimbingRace::syncWithBaseStation); + this->baseStationSyncTimer->start(); + + this->timerTextRefreshTimer = new QTimer(); + this->timerTextRefreshTimer->setInterval(1); + this->timerTextRefreshTimer->setSingleShot(true); + this->timerTextRefreshTimer->connect(this->timerTextRefreshTimer, &QTimer::timeout, this, &ClimbingRace::refreshTimerText); + this->refreshTimerText(); +} + +// -------------------------- +// --- Main Functionality --- +// -------------------------- + +int ClimbingRace::startRace() { + + if(this->state != IDLE) { + return 904; + } + + this->refreshMode(); + + qDebug() << "+ --- starting race"; + + int returnCode = 900; + + switch (this->mode) { + case LOCAL: + { + + this->setState(STARTING); + + this->nextStartAction = -1; + this->playSoundsAndStartRace(); + + returnCode = 200; + + break; + } + case REMOTE: + { + QVariantMap reply = this->baseConn->sendCommand(1000); + + if(reply["status"] != 200){ + //handle Error!! + returnCode = reply["status"].toInt(); + } + else { + + returnCode = 200; + + } + + break; + } + } + + return returnCode; +} + +int ClimbingRace::stopRace(int type) { + + if(this->state != RUNNING && this->state != STARTING) { + return 904; + } + + // type can be: + // 0: stopp + // 1: cancel + // 2: fail (fase start) + + this->refreshMode(); + + qDebug() << "+ --- stopping race"; + + int returnCode = 900; + + switch (this->mode) { + case LOCAL: + { + + if(type == 1){ + this->nextStartActionTimer->stop(); + this->player->stop(); + this->nextStartAction = -1; + } + + returnCode = this->speedTimers[0]->stop(type) ? 200:904; + + if(returnCode == 200) { + this->setState(STOPPED); + } + + break; + } + case REMOTE: + { + QVariantMap reply = this->baseConn->sendCommand(1001); + + if(reply["status"] != 200){ + //handle Error!! + returnCode = reply["status"].toInt(); + } + else { + returnCode = 200; + } + + break; + } + } + + return returnCode; +} + +int ClimbingRace::resetRace() { + + if(this->state != STOPPED) { + return 904; + } + + this->refreshMode(); + + qDebug() << "+ --- resetting race"; + + int returnCode = 900; + + + switch (this->mode) { + case LOCAL: + { + returnCode = this->speedTimers[0]->reset() ? 200:904; + + if(returnCode == 200){ + this->setState(IDLE); + } + + break; + } + case REMOTE: + { + + QVariantMap reply = this->baseConn->sendCommand(1002); + + if(reply["status"] != 200){ + //handle Error!! + returnCode = reply["status"].toInt(); + } + else { + returnCode = 200; + } + + break; + } + } + + return returnCode; +} + +// ------------------------- +// --- Base Station sync --- +// ------------------------- + +void ClimbingRace::syncWithBaseStation() { + this->refreshMode(); + + if(this->mode != REMOTE){ + this->baseStationSyncTimer->start(); + return; + } + + this->baseConn->refreshConnections(); + + QVariantMap tmpReply = this->baseConn->sendCommand(2000); + + if(tmpReply["status"] != 200){ + this->baseStationSyncTimer->start(); + return; + } + + this->setState( raceState( tmpReply["data"].toInt() ) ); + + switch (this->state) { + case 1: + { + // case STARTING + this->refreshRemoteTimers(); + + tmpReply = this->baseConn->sendCommand(2005); + if(tmpReply["status"] != 200){ + //handle error!! + qDebug() << "+ --- getting next start action progress from basestation failed"; + this->baseStationSyncTimer->start(); + return; + } + else { + this->nextStartActionDelayProgress = tmpReply["data"].toDouble() > 0 ? tmpReply["data"].toDouble():0; + this->nextStartActionDelayProgressChanged(); + } + + break; + } + default: + { + + this->refreshRemoteTimers(); + + break; + } + } + + this->baseStationSyncTimer->start(); +} + +// ------------------------ +// --- helper functions --- +// ------------------------ + +void ClimbingRace::playSoundsAndStartRace() { + qDebug() << "next Action: " << nextStartAction; + + nextStartActionTimer->disconnect(nextStartActionTimer, SIGNAL(timeout()), this, SLOT(playSoundsAndStartRace())); + + switch (this->nextStartAction) { + case 0: + { + if(!playSound("qrc:/sounds/at_marks_1.wav")){ + return; + } + if(appSettings->loadSetting("ready_en") == "true"){ + nextStartAction = 1; + nextStartActionTimer->setInterval(appSettings->loadSetting("ready_delay").toInt() <= 0 ? 1:appSettings->loadSetting("ready_delay").toInt()); + } + else{ + nextStartAction = 2; + nextStartActionTimer->setInterval(1); + } + + break; + } + case 1: + { + if(!playSound("qrc:/sounds/ready_1.wav")){ + return; + } + nextStartAction = 2; + nextStartActionTimer->setInterval(1); + + break; + } + case 2: + { + if(!playSound("qrc:/sounds/OFFICAL_IFSC_STARTIGNAL.wav")){ + return; + } + nextStartAction = -1; + nextStartActionTimer->disconnect(nextStartActionTimer, SIGNAL(timeout()), this, SLOT(playSoundsAndStartRace())); + + this->setState(RUNNING); + speedTimers[0]->start(); + + return; + } + default: + { + this->speedTimers[0]->setState(SpeedTimer::STARTING); + if(appSettings->loadSetting("at_marks_en") == "true"){ + nextStartAction = 0; + nextStartActionTimer->setInterval(appSettings->loadSetting("at_marks_delay").toInt() <= 0 ? 1:appSettings->loadSetting("at_marks_delay").toInt()); + } + else if(appSettings->loadSetting("ready_en") == "true"){ + nextStartAction = 1; + nextStartActionTimer->setInterval(appSettings->loadSetting("ready_delay").toInt() <= 0 ? 1:appSettings->loadSetting("ready_delay").toInt()); + } + else{ + nextStartAction = 2; + nextStartActionTimer->setInterval(1); + } + + break; + } + } + + nextStartActionTimer->connect(nextStartActionTimer, SIGNAL(timeout()), this, SLOT(playSoundsAndStartRace())); + nextStartActionTimer->start(); +} + +bool ClimbingRace::playSound(QString path) { + + player->setMedia(QUrl(path)); + player->setVolume(50); + player->play(); + + QTimer timer; + timer.setInterval(1); + timer.setSingleShot(true); + + QEventLoop loop; + loop.connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + + while (player->mediaStatus() == QMediaPlayer::LoadingMedia || player->mediaStatus() == QMediaPlayer::BufferingMedia || player->mediaStatus() == QMediaPlayer::BufferedMedia) { + timer.start(); + loop.exec(); + } + + if(player->mediaStatus() == QMediaPlayer::EndOfMedia){ + return true; + } + else { + return false; + } +} + +void ClimbingRace::setState(raceState newState) { + + if(newState != this->state) { + this->state = newState; + this->stateChanged(newState); + } +} + +void ClimbingRace::refreshMode() { + raceMode newMode; + if(this->baseConn->state == "connected"){ + newMode = REMOTE; + } + else { + newMode = LOCAL; + } + + if(this->mode != newMode){ + + if(newMode == LOCAL){ + // if the new mode is local -> connection to base station has been lost + + // go back to one timer + for (int i = 0;ispeedTimers.length();i++) { + delete this->speedTimers[i]; + } + + this->speedTimers.clear(); + + this->speedTimers.append(new SpeedTimer); + + // clear extensions + this->baseConn->connections.clear(); + } + + this->mode = newMode; + emit this->modeChanged(); + } + +} + +void ClimbingRace::refreshTimerText() { + + // --- refresh timer text --- + + QVariantList newTimerTextList; + + foreach(SpeedTimer * timer, this->speedTimers){ + QVariantMap timerMap = {{"text",timer->getText()}, {"reacttime", timer->reactionTime}}; + newTimerTextList.append(timerMap); + } + + if(newTimerTextList != this->qmlTimers){ + this->qmlTimers = newTimerTextList; + emit timerTextChanged(); + } + + // --- refresh next start action delay progress --- + + if(this->mode == LOCAL){ + QString totalStr; + + if(nextStartAction == 0){ + totalStr = appSettings->loadSetting("at_marks_delay"); + } + else if (nextStartAction == 1) { + totalStr = appSettings->loadSetting("ready_delay"); + } + + double remaining = this->nextStartActionTimer->remainingTime(); + double total = totalStr.toDouble(); + //qDebug() << "DELAY_PROG: " << "total: " << total << " remaining: " << remaining << " prog: " << remaining / total; + if(remaining > 0){ + this->nextStartActionDelayProgress = remaining / total; + emit this->nextStartActionDelayProgressChanged(); + } + else { + this->nextStartActionDelayProgress = 0; + emit this->nextStartActionDelayProgressChanged(); + } + } + else if (this->mode == REMOTE && this->state == IDLE) { + this->nextStartActionDelayProgress = 0; + emit this->nextStartActionDelayProgressChanged(); + } + + this->timerTextRefreshTimer->start(); +} + +bool ClimbingRace::refreshRemoteTimers() { + // get current time + QVariantMap tmpReply = this->baseConn->sendCommand(2007); + + if(tmpReply["status"].toInt() != 200){ + //handle error!! + qDebug() << "+ --- getting timers from basestation failed"; + this->baseStationSyncTimer->start(); + return false; + } + else { + QVariantList timers = tmpReply["data"].toList(); + + if(timers.length() != speedTimers.length()){ + // local timers are out of sync + + // delete all current timers + foreach(SpeedTimer * locTimer, this->speedTimers){ + delete locTimer; + } + + speedTimers.clear(); + + foreach(QVariant remTimer, timers){ + // create a local timer for each remote timer + this->speedTimers.append(new SpeedTimer); + } + } + + foreach(QVariant remTimer, timers){ + int currId = remTimer.toMap()["id"].toInt(); + speedTimers[currId]->startTime = this->date->currentMSecsSinceEpoch() - remTimer.toMap()["currTime"].toDouble(); + speedTimers[currId]->stoppedTime = remTimer.toMap()["currTime"].toDouble(); + speedTimers[currId]->reactionTime = remTimer.toMap()["reactTime"].toDouble(); + + speedTimers[currId]->setState(SpeedTimer::timerState(remTimer.toMap()["state"].toInt())); + } + + return true; + } +} + +// - athlete management - + +QVariant ClimbingRace::getAthletes() { + QVariantMap reply = this->baseConn->sendCommand(4003); + + if(reply["status"] != 200){ + //handle Error!! + qDebug() << "+ --- error getting athletes: " << reply["status"]; + return false; + } + + QVariantMap tmpAthletes = reply["data"].toMap(); + + //qDebug() << tmpAthletes; + + return tmpAthletes; +} + +bool ClimbingRace::createAthlete(QString userName, QString fullName) { + + QVariant requestData = QVariantMap({{"fullName", fullName}, {"userName", userName}}); + + QVariantMap reply = this->baseConn->sendCommand(4001, requestData.toJsonValue()); + + if(reply["status"] != 200){ + //handle Error!! + qDebug() << "+ --- error creating athlete: " << reply["status"]; + return false; + } + + return true; +} + +bool ClimbingRace::deleteAthlete( QString userName ){ + + QVariant requestData = QVariantMap({{"userName", userName}}); + + QVariantMap reply = this->baseConn->sendCommand(4002, requestData.toJsonValue()); + + if(reply["status"] != 200){ + //handle Error!! + qDebug() << "+ --- error deleting athlete: " << reply["status"]; + return false; + } + + return true; + +} + +bool ClimbingRace::selectAthlete( QString userName){ + + QVariant requestData = QVariantMap({{"userName", userName}}); + + QVariantMap reply = this->baseConn->sendCommand(4000, requestData.toJsonValue()); + + if(reply["status"] != 200){ + //handle Error!! + qDebug() << "+ --- error selecting athlete: " << reply["status"]; + return false; + } + + return true; + +} + +QVariant ClimbingRace::getResults( QString userName ){ + QVariantMap reply = this->baseConn->sendCommand(4004, userName); + + if(reply["status"] != 200){ + //handle Error!! + qDebug() << "+ --- error getting results: " << reply["status"]; + return false; + } + + QVariantList tmpAthletes = reply["data"].toList(); + + //qDebug() << tmpAthletes; + + return tmpAthletes; +} + +// ------------------------- +// --- functions for qml --- +// ------------------------- + +int ClimbingRace::getState() { + return this->state; +} + +int ClimbingRace::getMode() { + return this->mode; +} + +QVariant ClimbingRace::getTimerTextList() { + return this->qmlTimers; +// QVariantList test; +// QVariantMap test2 = {{"text", "1234"}, {"reacttime", 2.0}}; +// test.append(test2); +// return test; +} + +double ClimbingRace::getNextStartActionDelayProgress() { + return this->nextStartActionDelayProgress; +} + +void ClimbingRace::writeSetting(QString key, QVariant value) { + this->refreshMode(); + + if(this->mode == REMOTE && ( this->remoteSettings.contains(key) || this->remoteOnlySettings.contains(key) ) ){ + this->baseConn->writeRemoteSetting(key, value.toString()); + } + else if(!this->remoteOnlySettings.contains(key)){ + this->appSettings->writeSetting(key, value); + } +} + +QString ClimbingRace::readSetting(QString key) { + this->refreshMode(); + + if(this->mode == REMOTE && ( this->remoteSettings.contains(key) || this->remoteOnlySettings.contains(key) )){ + QVariantMap reply = this->baseConn->sendCommand(3001, key); + if(reply["status"] != 200){ + return "false"; + } + return reply["data"].toString(); + } + else if(!this->remoteOnlySettings.contains(key)){ + return this->appSettings->loadSetting(key); + } + else { + return "false"; + } +} + +bool ClimbingRace::connectBaseStation() { + this->reloadBaseStationIpAdress(); + return this->baseConn->connectToHost(); +} + +void ClimbingRace::disconnectBaseStation() { + this->baseConn->closeConnection(); +} + +QString ClimbingRace::getBaseStationState() { + return this->baseConn->getState(); +} + +QVariant ClimbingRace::getBaseStationConnections() { + return baseConn->getConnections(); +} + + +bool ClimbingRace::reloadBaseStationIpAdress() { + if(this->baseConn->state == "disconnected"){ + this->baseConn->setIP(pGlobalAppSettings->loadSetting("baseStationIpAdress")); + return true; + } + return false; +} diff --git a/sources/main.cpp b/sources/main.cpp index cfa2fc0..5e51af7 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -23,7 +23,7 @@ #include #include #include -#include +//#include #include #include @@ -44,14 +44,17 @@ #include #include #include -#include +//#include #ifdef Q_OS_ANDROID #include #endif #include "headers/sqlstoragemodel.h" #include "headers/sqlprofilemodel.h" -#include "headers/buzzerconn.h" #include "headers/appsettings.h" +#include "headers/baseconn.h" +#include "headers/speedtimer.h" +#include "headers/climbingrace.h" +#include "headers/apptheme.h" #include static void connectToDatabase() @@ -64,7 +67,7 @@ static void connectToDatabase() } const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - qDebug() << writeDir; + if (!writeDir.mkpath(".")) qFatal("Failed to create writable directory at %s", qPrintable(writeDir.absolutePath())); @@ -105,14 +108,22 @@ int main(int argc, char *argv[]) #endif connectToDatabase(); - BuzzerConn * pBuzzerConn = new BuzzerConn(nullptr, "192.168.4.1", 80); - BuzzerConn * pStartpadConn = new BuzzerConn(nullptr, "192.168.43.150", 80); + AppSettings * pAppSettings = new AppSettings(); //setup the sql storage model as a qml model qmlRegisterType("com.itsblue.speedclimbingstopwatch", 1, 0, "SqlProfileModel"); qmlRegisterType("com.itsblue.speedclimbingstopwatch", 1, 0, "SqlStorageModel"); + //setup the startpad and buzzer conn qml objects + //qmlRegisterType("com.itsblue.speedclimbingstopwatch", 1, 0, "BuzzerConn"); + //qmlRegisterType("com.itsblue.speedclimbingstopwatch", 1, 0, "StartpadConn"); + //qmlRegisterType("com.itsblue.speedclimbingstopwatch", 1, 0, "BaseStationConn"); + //qmlRegisterType("com.itsblue.speedclimbingstopwatch", 1, 0, "SpeedTimerBackend"); + + qmlRegisterType("com.itsblue.speedclimbingstopwatch", 2, 0, "SpeedBackend"); + qmlRegisterType("com.itsblue.speedclimbingstopwatch", 2, 0, "AppTheme"); + //setup translation engine //to the language of the system //if the system language is not found the language is set to english @@ -122,19 +133,15 @@ int main(int argc, char *argv[]) QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + + QQmlContext *context = engine.rootContext(); + context->setContextProperty("_cppAppSettings", pAppSettings); + if (engine.rootObjects().isEmpty()) return -1; - engine.rootContext()->setContextProperty("_cppBuzzerConn", pBuzzerConn); - engine.rootContext()->setContextProperty("_cppStartpadConn", pStartpadConn); - engine.rootContext()->setContextProperty("_cppAppSettings", pAppSettings); - int iRet = 0; iRet = app.exec(); - delete pBuzzerConn; - delete pStartpadConn; - delete pAppSettings; - return iRet; } diff --git a/sources/speedtimer.cpp b/sources/speedtimer.cpp new file mode 100644 index 0000000..549d647 --- /dev/null +++ b/sources/speedtimer.cpp @@ -0,0 +1,194 @@ +#include "headers/speedtimer.h" + +SpeedTimer::SpeedTimer(QObject *parent) : QObject(parent) +{ + + this->date = new QDateTime; + + this->startTime = 0; + this->stopTime = 0; + this->stoppedTime = 0; + this->reactionTime = 0; + this->state = IDLE; +} + +bool SpeedTimer::start(bool force) { + if(this->state != STARTING && !force){ + return false; + } + qDebug() << "starting timer"; + if(!force){ + this->stopTime = 0; + this->stoppedTime = 0; + this->reactionTime = 0; + this->startTime = this->date->currentMSecsSinceEpoch(); + } + + this->setState(RUNNING); + + return true; +} + +bool SpeedTimer::stop(int type, bool force) { + + // type can be: + // 0: stopped + // 1: cancelled + // 2: failed (fase start) + + if( ( this->state != SpeedTimer::STARTING && this->state != SpeedTimer::RUNNING && this->state ) && !force ){ + return false; + } + + //qDebug() << "Stopping: " << "start Time: " << startTime << " stopTime: " << stopTime << " stoppedTime: " << stoppedTime << " reactionTime: " << reactionTime; + + switch (type) { + case 0: + { + this->stopTime = this->date->currentMSecsSinceEpoch(); + this->stoppedTime = this->stopTime - this->startTime; + this->setState(STOPPED); + break; + } + case 1: + { + this->stoppedTime = 0; + this->setState(CANCELLED); + break; + } + case 2: + { + this->stoppedTime = this->reactionTime; + this->setState(FAILED); + break; + } + } + + qDebug() << "Stopped: " << "start Time: " << startTime << " stopTime: " << stopTime << " stoppedTime: " << stoppedTime << " reactionTime: " << reactionTime; + + return true; + //this->startPad->appendCommand("SET_LED_STARTING"); +} + +bool SpeedTimer::reset(bool force){ + if( ( this->state != STOPPED && this->state != FAILED && this->state != CANCELLED ) && !force){ + return false; + } + + this->startTime = 0; + this->stopTime = 0; + this->stoppedTime = 0; + this->reactionTime = 0; + this->setState(IDLE); + + return true; + //this->startPad->appendCommand("SET_LED_STARTING"); +} + +void SpeedTimer::setState(timerState newState){ + if(this->state != newState){ + this->state = newState; + qDebug() << "+--- timer state changed: " << newState; + emit this->stateChanged(newState); + } +} + +QString SpeedTimer::getState(){ + switch(state){ + case IDLE: + return("IDLE"); + case STARTING: + return("STARTING"); + case WAITING: + return ("WAITING"); + case RUNNING: + return("RUNNING"); + case STOPPED: + return("STOPPED"); + case FAILED: + return("FAILED"); + case CANCELLED: + return("CANCELLED"); + } +} + +double SpeedTimer::getCurrTime() { + double currTime; + if(this->state == RUNNING && this->startTime > 0){ + currTime = this->date->currentMSecsSinceEpoch() - this->startTime; + } + else { + currTime = this->stoppedTime; + } + + return(currTime); +} + +QString SpeedTimer::getText() { + //qDebug() << this->getState(); + QString newText; + switch (this->state) { + case SpeedTimer::IDLE: + newText = tr("Click Start to start"); + break; + case SpeedTimer::STARTING: + newText = "0.000 sec"; + break; + case SpeedTimer::WAITING: + newText = tr("Please wait..."); + break; + case SpeedTimer::RUNNING: + newText = QString::number( this->getCurrTime() / 1000.0, 'f', 3 ) + " sec"; + break; + case SpeedTimer::STOPPED: + newText = QString::number( this->stoppedTime / 1000.0, 'f', 3 ) + " sec"; + break; + case SpeedTimer::FAILED: + newText = tr("False Start"); + break; + case SpeedTimer::CANCELLED: + newText = tr("Cancelled"); + break; + } + + return newText; +} + +void SpeedTimer::delay(int mSecs){ + QEventLoop loop; + QTimer timer; + + timer.setSingleShot(true); + // quit the loop when the timer times out + loop.connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + //quit the loop when the connection was established + // start the timer before starting to connect + timer.start(mSecs); + //connect + + //wait for the connection to finish (programm gets stuck in here) + loop.exec(); +} + +SpeedTimer::timerState SpeedTimer::stateFromString(QString state){ + + if(state == "IDLE"){ + return IDLE; + } + else if (state == "STARTING") { + return STARTING; + } + else if (state == "RUNNING") { + return RUNNING; + } + else if (state == "STOPPED") { + return STOPPED; + } + else if (state == "FAILED") { + return FAILED; + } + else { + return CANCELLED; + } +} + diff --git a/speedclimbing_stopwatch.pro b/speedclimbing_stopwatch.pro index 52665f7..8f86ecc 100644 --- a/speedclimbing_stopwatch.pro +++ b/speedclimbing_stopwatch.pro @@ -1,9 +1,10 @@ -QT += quick sql +QT += quick sql multimedia android { QT += androidextras } VERSION = 0.04 +DEFINES += APP_VERSION=$$VERSION CONFIG += c++11 # The following define makes your compiler emit warnings if you use @@ -23,14 +24,20 @@ SOURCES += \ sources/main.cpp \ sources/sqlstoragemodel.cpp \ sources/sqlprofilemodel.cpp \ - sources/buzzerconn.cpp \ - sources/appsettings.cpp + sources/appsettings.cpp \ + sources/baseconn.cpp \ + sources/speedtimer.cpp \ + sources/climbingrace.cpp \ + sources/apptheme.cpp HEADERS += \ headers/sqlstoragemodel.h \ headers/sqlprofilemodel.h \ - headers/buzzerconn.h \ - headers/appsettings.h + headers/appsettings.h \ + headers/baseconn.h \ + headers/speedtimer.h \ + headers/climbingrace.h \ + headers/apptheme.h RESOURCES += \ shared.qrc \ @@ -49,7 +56,8 @@ QTPLUGIN += qtaudio_coreaudio # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin -else: unix:!android: target.path = /opt/$${TARGET}/bin +#else: unix:!android: target.path = /opt/$${TARGET}/bin +else: unix:!android: target.path = /home/pi/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target DISTFILES += \ diff --git a/translations/de_DE.qm b/translations/de_DE.qm index 1e9ac75..40df268 100644 Binary files a/translations/de_DE.qm and b/translations/de_DE.qm differ diff --git a/translations/de_DE.ts b/translations/de_DE.ts index 6411f5f..96b648c 100644 --- a/translations/de_DE.ts +++ b/translations/de_DE.ts @@ -1,106 +1,193 @@ + + InputDelegate + + delay (ms) + Verzögerung (ms) + + SettingsDialog - + Options Optionen - - connected to buzzer - Mit Buzzer verbunden + Mit Buzzer verbunden - - connect to buzzer - Mit Buzzer verbinden + Mit Buzzer verbinden - + connecting... verbinde... - success! - Erfolg + Erfolg - error! - Fehler + Fehler - + + + Base Station + Base Station + + + start sequence Start Ablauf - + + dark mode + dunkler Modus + + + say 'ready' sage 'ready' - - + + delay (ms) Verzögerung (ms) - - + + time Zeit - + + say 'at your marks' + sage 'at your marks' + + + + disconnect + trennen + + + + connect + verbinden + + + + IP-Adress + IP-Adresse + + + + volume + Lautstärke + + + + connected extensions + verbundene Erweiterungen + + + + connections + Verbindungen + + say 'at your marks' - sage + sage 'at your marks' + + SpeedTimer + + + Click Start to start + Tippe start zum Starten + + + + Please wait... + Bitte warten... + + + + False Start + Fehlstart + + + + Cancelled + Abgebrochen + + main - + Speedclimbing stw - - Click start to start - Tippe start zum Starten + Tippe start zum Starten - - + + reaction time (ms): + Reaktionszeit (ms): + + + + start start - + cancel Abbruch - + + Click Start to start + Tippe start zum Starten + + + + waiting... + warte... + + + + please wait... + Bitte warten... + + + + starting... starte... - false start - Fehlstart + Fehlstart - + reset reset