diff --git a/CHANGELOG b/CHANGELOG index 8f4a85e..074c2a6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,10 @@ 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 +- cancel button during start sequence +- new screen in landscape mode +- buttons for settings and profiles ## [0.02] - 2018-07-18 ### Fixed @@ -17,4 +21,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. prevent it from getting out of the screen ## [0.01] -### Initial Release \ No newline at end of file +### Initial Release diff --git a/android-sources/AndroidManifest.xml b/android-sources/AndroidManifest.xml index f4a90c4..c28cb64 100644 --- a/android-sources/AndroidManifest.xml +++ b/android-sources/AndroidManifest.xml @@ -1,7 +1,7 @@ - + - + @@ -59,6 +59,7 @@ --> + diff --git a/android-sources/src/StayAwake.java b/android-sources/src/StayAwake.java new file mode 100644 index 0000000..8fc70e6 --- /dev/null +++ b/android-sources/src/StayAwake.java @@ -0,0 +1,8 @@ +package com.itsblue; +public class StayAwake extends org.qtproject.qt5.android.bindings.QtActivity { + @Override + public void onCreate(android.os.Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + this.getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } +} diff --git a/graphics/icons/settings.png b/graphics/icons/settings.png new file mode 100644 index 0000000..c818eb3 Binary files /dev/null and b/graphics/icons/settings.png differ diff --git a/graphics/icons/user.png b/graphics/icons/user.png new file mode 100644 index 0000000..2255aa9 Binary files /dev/null and b/graphics/icons/user.png differ diff --git a/graphics/screenshots/photo_2018-07-17_17-36-30.jpg b/graphics/screenshots/photo_2018-07-17_17-36-30.jpg new file mode 100644 index 0000000..9ce1075 Binary files /dev/null and b/graphics/screenshots/photo_2018-07-17_17-36-30.jpg differ diff --git a/graphics/screenshots/photo_2018-07-17_17-50-50.jpg b/graphics/screenshots/photo_2018-07-17_17-50-50.jpg new file mode 100644 index 0000000..2000252 Binary files /dev/null and b/graphics/screenshots/photo_2018-07-17_17-50-50.jpg differ diff --git a/main.cpp b/main.cpp index 6333b85..472f55d 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,41 @@ #include #include +#include +#include +#include +#include +#include +#include +#include + +#include "sqlstoragemodel.h" +#include "sqlprofilemodel.h" + +static void connectToDatabase() +{ + QSqlDatabase database = QSqlDatabase::database(); + if (!database.isValid()) { + database = QSqlDatabase::addDatabase("QSQLITE"); + if (!database.isValid()) + qFatal("Cannot add database: %s", qPrintable(database.lastError().text())); + } + + const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + qDebug() << writeDir; + if (!writeDir.mkpath(".")) + qFatal("Failed to create writable directory at %s", qPrintable(writeDir.absolutePath())); + + // Ensure that we have a writable location on all devices. + const QString fileName = writeDir.absolutePath() + "/chat-database.sqlite3"; + //QFile::remove(fileName); + // When using the SQLite driver, open() will create the SQLite database if it doesn't exist. + database.setDatabaseName(fileName); + if (!database.open()) { + QFile::remove(fileName); + qFatal("Cannot open database: %s", qPrintable(database.lastError().text())); + + } +} int main(int argc, char *argv[]) { @@ -7,6 +43,13 @@ int main(int argc, char *argv[]) QGuiApplication app(argc, argv); + + connectToDatabase(); + + //setup the sql storage model as a qml model + qmlRegisterType("com.itsblue.speedclimbingstopwatch", 1, 0, "SqlProfileModel"); + qmlRegisterType("com.itsblue.speedclimbingstopwatch", 1, 0, "SqlStorageModel"); + QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) diff --git a/main.qml b/main.qml index 5488874..4d7ed95 100644 --- a/main.qml +++ b/main.qml @@ -2,6 +2,9 @@ import QtQuick 2.9 import QtMultimedia 5.8 import QtQuick.Window 2.2 import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.11 + +import com.itsblue.speedclimbingstopwatch 1.0 Window { visible: true @@ -20,7 +23,10 @@ Window { property double stoppedTime: 0 property double currTime: new Date().getTime() + state: "IDLE" + Timer { + //timer that updates the currTime variable running: true repeat: true interval: 1 @@ -29,36 +35,11 @@ Window { } } - Item { - id: time_container - anchors { - top: parent.top - left: parent.left - right: parent.right - } - height: parent.height * 0.15 - - Label { - id: time - text: "Click start to start" - - anchors.centerIn: parent - font.pixelSize: parent.height * 0.3 - } - } - - - Rectangle { - width: parent.width - height: 1 - color: "grey" - anchors.left: parent.left - anchors.top: time_container.bottom - } - SoundEffect { + //start sound id: startSound source: "OFFICAL_IFSC_STARTIGNAL.wav" + onPlayingChanged: { if(!playing){ root.startTime = new Date().getTime() @@ -69,18 +50,56 @@ Window { } } + /*------------------------ + Timer text an upper line + ------------------------*/ + Item { + id: time_container + anchors { + top: parent.top + left: parent.left + right: root.landscape() ? startButt.left:parent.right + bottom: root.landscape() ? parent.bottom:startButt.top + bottomMargin: root.landscape() ? undefined:parent.height * 0.1 + rightMargin: root.landscape() ? parent.width * 0.05:0 + } + //height: root.landscape() ? undefined:parent.height * 0.15 + Label { + id: time + text: "Click start to start" + + anchors.centerIn: parent + //font.pixelSize: root.landscape() ? parent.width * 0.1:parent.height * 0.3 + elide: "ElideRight" + } + } + + Rectangle { + width: root.landscape() ? 1:parent.width + height: root.landscape() ? parent.height:1 + color: "grey" + anchors.left: root.landscape() ? time_container.right:parent.left + anchors.top: root.landscape() ? parent.top:time_container.bottom + anchors.bottom: root.landscape() ? parent.bottom:undefined + } + + /*---------------------- + Start button + ----------------------*/ Rectangle { id: startButt property string text: "start" + property int size: root.landscape() ? parent.width * 0.5:parent.height * 0.5 anchors { - horizontalCenter: parent.horizontalCenter bottom: parent.bottom bottomMargin: parent.height * 0.5 - height * 0.5 + right: parent.right + rightMargin: parent.width * 0.5 - width * 0.5 } - height: parent.height - (parent.height * 0.5) - width: height > parent.width ? parent.width * 0.8:height + 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 color: "white" border.color: "grey" border.width: 1 @@ -100,67 +119,352 @@ Window { onReleased: parent.color = "white" onClicked: { switch(root.state) { - case "": - root.state = "IDLE" - case "IDLE": - root.state = "STARTING" - startSound.play() - break - case "RUNNING": - root.stoppedTime = new Date().getTime() - root.startTime - time.text = ( root.stoppedTime / 1000 ).toFixed(3) + " sec" - root.state = "STOPPED" - break - case "STOPPED": - root.state = "IDLE" - break + case "": + root.state = "IDLE" + case "IDLE": + root.state = "STARTING" + startSound.play() + break + case "RUNNING": + root.stoppedTime = new Date().getTime() - root.startTime + time.text = ( root.stoppedTime / 1000 ).toFixed(3) + " sec" + root.state = "STOPPED" + break + case "STOPPED": + root.state = "IDLE" + break } } } } - states: [ - State { - name: "IDLE" - //state for the start page - PropertyChanges { target: time; text: "Click start to start"; font.pixelSize: parent.height * 0.3 } - PropertyChanges { target: time_container; height: parent.height * 0.15 } - PropertyChanges { target: startButt; enabled: true; text: "start"; height: parent.height - (parent.height * 0.5); anchors.bottomMargin: parent.height * 0.5 - startButt.height * 0.5 } - }, - State { - name: "STARTING" - //state for the start sequence - PropertyChanges { target: startButt; enabled: false; text: "starting..." } - PropertyChanges { target: time; text: "0.000 sec" } - }, - State { - name: "RUNNING" - //state when the timer is running - PropertyChanges { target: time; text: ( ( root.currTime - root.startTime ) / 1000 ).toFixed(3) + " sec" } - PropertyChanges { target: startButt; enabled: true; text: "stop" } - }, + /*---------------------- + Cancel button + ----------------------*/ + Rectangle { + id: cancelButt - State { - name: "STOPPED" - //state when the meassuring is over - PropertyChanges { target: time; text: ( root.stoppedTime / 1000 ).toFixed(3) + " sec"; font.pixelSize: parent.height * 0.1 } - PropertyChanges { target: startButt; enabled: true; text: "reset"; height: parent.height - (parent.height * 0.8); anchors.bottomMargin: parent.height * 0.2 - startButt.height * 0.5 } - PropertyChanges { target: time_container; height: parent.height * 0.8 } + property string text: "cancel" + anchors { + right: startButt.right + bottom: startButt.bottom + } + height: startButt.height * 0.3 + scale: 0 + width: height + color: "white" + border.color: "grey" + border.width: 1 + radius: width / 2 + + Label { + id: cancelButt_text + text: parent.text + anchors.centerIn: parent + font.pixelSize: parent.height * 0.16 + font.family: "Helvetica" + } + MouseArea { + enabled: startSound.playing + anchors.fill: parent + onPressed: parent.color = "lightgrey" + onReleased: parent.color = "white" + onClicked: { + startSound.stop() + root.stoppedTime = 0 + time.text = "false start" + root.state = "STOPPED" } - ] + } + + Behavior on scale { + PropertyAnimation { + duration: 200 + } + } + } + + /*------------------- + lower line and menu + -------------------*/ + Rectangle { + width: root.landscape() ? 1:parent.width + height: root.landscape() ? parent.height:1 + color: "grey" + 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 + } + + Item { + id: menu_container + anchors { + bottom: parent.bottom + right: parent.right + left: root.landscape() ? startButt.right:parent.left + top: root.landscape() ? parent.top:startButt.bottom + topMargin: root.landscape() ? undefined:parent.height * 0.1 + leftMargin: root.landscape() ? parent.width * 0.05:0 + } + + Rectangle { + id: settingsButt + + property string text: "cancel" + 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 + topMargin: root.landscape() ? (parent.height - (height * 2)) / 3:undefined + //align in portrait mode + leftMargin: root.landscape() ? undefined:(parent.width - width * 2) / 3 + + + } + height: root.landscape() ? parent.width * 0.7:parent.height * 0.7 + width: height + color: "white" + border.color: "grey" + border.width: 1 + radius: width / 2 + + + Image { + id: settungsButt_Image + source: "qrc:/graphics/icons/settings.png" + anchors.centerIn: parent + height: parent.height * 0.7 + width: parent.width * 0.7 + mipmap: true + } + + MouseArea { + enabled: root.state === "IDLE" + anchors.fill: parent + onPressed: parent.color = "lightgrey" + onReleased: parent.color = "white" + onClicked: { + } + } + + Behavior on scale { + PropertyAnimation { + duration: 200 + } + } + } + Rectangle { + id: profilesButt + + property string text: "cancel" + 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 + } + height: root.landscape() ? parent.width * 0.7:parent.height * 0.7 + + width: height + color: "white" + border.color: "grey" + border.width: 1 + radius: width / 2 + + 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 + } + + MouseArea { + enabled: root.state === "IDLE" + anchors.fill: parent + onPressed: parent.color = "lightgrey" + onReleased: parent.color = "white" + onClicked: { + } + } + + Behavior on scale { + PropertyAnimation { + duration: 200 + } + } + } + + } + + + /* + // ComboBox { + // id: profileBox + // property int profileIndex: -1 + // model: SqlProfileModel{} + // textRole: "name" + // width: parent.width + // anchors { + // bottom: parent.bottom + // horizontalCenter: parent.horizontalCenter + // } + // height: parent.height * 0.05 + // background: Rectangle { + // color: profileBox.down ? "lightgrey":"white" + // border.width: 1 + // border.color: "grey" + // } + // popup: Popup { + // id: profileBox_popup + // property bool popup_open: false + // y: profileBox.height - 1 + // width: profileBox.width + // implicitHeight: contentItem.implicitHeight + // padding: 1 + + // contentItem: ListView { + // clip: true + // implicitHeight: contentHeight + // model: profileBox.popup.visible ? profileBox.delegateModel : null + // currentIndex: profileBox.highlightedIndex + + // ScrollIndicator.vertical: ScrollIndicator { } + // } + // onVisibleChanged: { + // if(visible){ + // if(popup_open){ + // popup_open = false + // return + // } + // popup_open = true + // height=implicitHeight + // } + // else { + // height = 0 + // visible = true + // } + // } + + // background: Rectangle { + // border.color: "grey" + // radius: 2 + // } + + // Behavior on height { + // NumberAnimation + // { + // onRunningChanged: { + // if(!running && !profileBox_popup.popup_pen){ + // profileBox_popup.popup_open = false + // profileBox_popup.visible = false + // } + // } + + // duration: 200 + // } + // } + // } + + // onDownChanged: { + // if(profileIndex !== currentIndex) + // profileIndex = currentIndex + // } + // } + */ + + + /*---------------------- + Timer states + ----------------------*/ + states: [ + State { + name: "IDLE" + //state for the start page + PropertyChanges { target: time; text: "Click start to start"; font.pixelSize: root.landscape() ? parent.width * 0.1:parent.height * 0.3; scale: 1 } + PropertyChanges { + target: time_container; + anchors.bottomMargin: root.landscape() ? undefined:parent.height * 0.1; + anchors.rightMargin: root.landscape() ? parent.height * 0.05:0 + } + + PropertyChanges { + target: startButt; + enabled: true; text: "start"; + size: root.landscape() ? parent.width * 0.5:parent.height * 0.5 + anchors.bottomMargin: parent.height * 0.5 - startButt.height * 0.5 + anchors.rightMargin: parent.width * 0.5 - startButt.width * 0.5 + } + + }, + State { + name: "STARTING" + //state for the start sequence + PropertyChanges { target: startButt; enabled: false; text: "starting..."; + anchors.rightMargin: root.landscape() ? parent.width * 0.05:none //put the button more to the right to hide the menu (only in landscape mode) + anchors.bottomMargin: root.landscape() ? none: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; } + }, + State { + name: "RUNNING" + //state when the timer is running + PropertyChanges { target: time; text: ( ( 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:none //put the button more to the right to hide the menu (only in landscape mode) + anchors.bottomMargin: root.landscape() ? none:parent.height * 0.1 //put the button lower to hide the menu (only in portrait mode) + } + + }, + + State { + name: "STOPPED" + //state when the meassuring is over + PropertyChanges { + target: time; text: root.stoppedTime > 0 ? ( root.stoppedTime / 1000 ).toFixed(3) + " sec":"false start"; + font.pixelSize: root.landscape() ? parent.width * 0.15:parent.height * 0.1; + scale: 1 + } + PropertyChanges { + target: startButt; + enabled: true; text: "reset"; + size: root.landscape() ? parent.height * 0.35:parent.height * 0.2; + anchors.bottomMargin: root.landscape() ? parent.height * 0.5 - startButt.height * 0.5:parent.height * 0.2 - startButt.height * 0.5 + anchors.rightMargin: root.landscape() ? parent.height * 0.2 - startButt.height * 0.5:parent.width * 0.5 - startButt.width * 0.5 + } + PropertyChanges { + target: time_container; + anchors.rightMargin: root.landscape() ? 0-startButt.width/2:undefined + anchors.bottomMargin: root.landscape() ? undefined:0-startButt.height/2 + } + } + ] + + /*---------------------- + Timer animations + ----------------------*/ transitions: [ Transition { - NumberAnimation { properties: "height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } - FadeAnimation { target: time; fadeDuration_in: 0; fadeDuration_out: 0 } + NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } }, Transition { to: "STOPPED" - NumberAnimation { properties: "height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } + NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } }, Transition { to: "IDLE" - NumberAnimation { properties: "height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } + NumberAnimation { properties: "size,rightMargin,height,width,bottomMargin,font.pixelSize"; easing.type: Easing.InOutQuad; duration: 700 } FadeAnimation { target: time; fadeDuration_out: 1000; fadeDuration_in: 0} }, @@ -169,5 +473,12 @@ Window { //disable transitions for the RUNNING state } ] + + /*---------------------- + Timer functions + ----------------------*/ + function landscape(){ + return(root.height < root.width) + } } } diff --git a/shared.qrc b/shared.qrc index 8a7ba26..4ffc9a4 100644 --- a/shared.qrc +++ b/shared.qrc @@ -1,5 +1,7 @@ OFFICAL_IFSC_STARTIGNAL.wav + graphics/icons/settings.png + graphics/icons/user.png diff --git a/speedclimbing_stopwatch.pro b/speedclimbing_stopwatch.pro index 24c11c3..0b850f3 100644 --- a/speedclimbing_stopwatch.pro +++ b/speedclimbing_stopwatch.pro @@ -1,4 +1,4 @@ -QT += quick +QT += quick sql androidextras CONFIG += c++11 # The following define makes your compiler emit warnings if you use @@ -15,7 +15,9 @@ DEFINES += QT_DEPRECATED_WARNINGS TARGET = speedclimbing_stw SOURCES += \ - main.cpp + main.cpp \ + sqlstoragemodel.cpp \ + sqlprofilemodel.cpp RESOURCES += qml.qrc \ shared.qrc @@ -35,8 +37,13 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target DISTFILES += \ - android-sources/AndroidManifest.xml + android-sources/AndroidManifest.xml \ + android-sources/src/StayAwake.java android { ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android-sources } + +HEADERS += \ + sqlstoragemodel.h \ + sqlprofilemodel.h diff --git a/sqlprofilemodel.cpp b/sqlprofilemodel.cpp new file mode 100644 index 0000000..19a1eb2 --- /dev/null +++ b/sqlprofilemodel.cpp @@ -0,0 +1,56 @@ +#include "sqlprofilemodel.h" + +static void createTable() +{ + if (QSqlDatabase::database().tables().contains(QStringLiteral("Contacts"))) { + // The table already exists; we don't need to do anything. + return; + } + + QSqlQuery query; + + //creat eth etable to store the profiles + if (!query.exec( + "CREATE TABLE IF NOT EXISTS `profiles` ( " + " `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE," + " `name` TEXT NOT NULL " + " );")) { + qFatal("Failed to query database: %s", qPrintable(query.lastError().text())); + } + + //create the table to store the times + if (!query.exec( + "CREATE TABLE IF NOT EXISTS `times` (" + " `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE," + " `profileid` INTEGER NOT NULL," + " `time` INTEGER NOT NULL" + " );")) { + qFatal("Failed to query database: %s", qPrintable(query.lastError().text())); + } +} + +SqlProfileModel::SqlProfileModel(QObject *parent) : QSqlTableModel(parent) +{ + qDebug("ProfileModel constructor"); + createTable(); + setTable("profiles"); + select(); +} + +QVariant SqlProfileModel::data(const QModelIndex &index, int role) const +{ + if (role < Qt::UserRole) + return QSqlTableModel::data(index, role); + + const QSqlRecord sqlRecord = record(index.row()); + return sqlRecord.value(role - Qt::UserRole); +} + +QHash SqlProfileModel::roleNames() const +{ + QHash names; + names[Qt::UserRole + 0] = "id"; + names[Qt::UserRole + 1] = "name"; + + return names; +} diff --git a/sqlprofilemodel.h b/sqlprofilemodel.h new file mode 100644 index 0000000..80a96d4 --- /dev/null +++ b/sqlprofilemodel.h @@ -0,0 +1,25 @@ +#ifndef SQLPROFILEMODEL_H +#define SQLPROFILEMODEL_H + +#include +#include +#include +#include +#include +#include +#include + +class SqlProfileModel : public QSqlTableModel +{ + Q_OBJECT +public: + explicit SqlProfileModel(QObject *parent = nullptr); + + QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; + QHash roleNames() const Q_DECL_OVERRIDE; +signals: + +public slots: +}; + +#endif // SQLPROFILEMODEL_H diff --git a/sqlstoragemodel.cpp b/sqlstoragemodel.cpp new file mode 100644 index 0000000..6354d96 --- /dev/null +++ b/sqlstoragemodel.cpp @@ -0,0 +1,6 @@ +#include "sqlstoragemodel.h" + +SqlStorageModel::SqlStorageModel(QObject *parent) : QObject(parent) +{ + +} diff --git a/sqlstoragemodel.h b/sqlstoragemodel.h new file mode 100644 index 0000000..cc5eede --- /dev/null +++ b/sqlstoragemodel.h @@ -0,0 +1,17 @@ +#ifndef SQLSTORAGEMODEL_H +#define SQLSTORAGEMODEL_H + +#include + +class SqlStorageModel : public QObject +{ + Q_OBJECT +public: + explicit SqlStorageModel(QObject *parent = nullptr); + +signals: + +public slots: +}; + +#endif // SQLSTORAGEMODEL_H