see changelog

This commit is contained in:
Dorian Zedler 2018-07-22 16:47:55 +02:00
parent 7255fe6cd7
commit 4b09f2a8e7
15 changed files with 561 additions and 81 deletions

View file

@ -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
### Initial Release

View file

@ -1,7 +1,7 @@
<?xml version="1.0"?>
<manifest package="com.itsblue.speedclimbing_stopwatch" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="0.02" android:versionCode="2" android:installLocation="auto">
<manifest package="com.itsblue.speedclimbing_stopwatchtest" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="0.02" android:versionCode="2" android:installLocation="auto">
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="speedclimbing stw" android:icon="@drawable/icon">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="speedclimbing stw" android:screenOrientation="unspecified" android:launchMode="singleTop">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation" android:name="com.itsblue.StayAwake" android:label="speedclimbing stw" android:screenOrientation="unspecified" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@ -59,6 +59,7 @@
-->
<meta-data android:name="android.app.extract_android_style" android:value="full"/>
<!-- extract android style -->
</activity>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->

View file

@ -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);
}
}

BIN
graphics/icons/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

BIN
graphics/icons/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -1,5 +1,41 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QSqlDatabase>
#include <QSqlError>
#include <QDebug>
#include <QFile>
#include <QDir>
#include <QStandardPaths>
#include <QtAndroidExtras>
#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<SqlProfileModel>("com.itsblue.speedclimbingstopwatch", 1, 0, "SqlProfileModel");
qmlRegisterType<SqlStorageModel>("com.itsblue.speedclimbingstopwatch", 1, 0, "SqlStorageModel");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())

461
main.qml
View file

@ -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)
}
}
}

View file

@ -1,5 +1,7 @@
<RCC>
<qresource prefix="/">
<file>OFFICAL_IFSC_STARTIGNAL.wav</file>
<file>graphics/icons/settings.png</file>
<file>graphics/icons/user.png</file>
</qresource>
</RCC>

View file

@ -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

56
sqlprofilemodel.cpp Normal file
View file

@ -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<int, QByteArray> SqlProfileModel::roleNames() const
{
QHash<int, QByteArray> names;
names[Qt::UserRole + 0] = "id";
names[Qt::UserRole + 1] = "name";
return names;
}

25
sqlprofilemodel.h Normal file
View file

@ -0,0 +1,25 @@
#ifndef SQLPROFILEMODEL_H
#define SQLPROFILEMODEL_H
#include <QObject>
#include <QDebug>
#include <QSqlError>
#include <QSqlQuery>
#include <QDateTime>
#include <QSqlRecord>
#include <QSqlTableModel>
class SqlProfileModel : public QSqlTableModel
{
Q_OBJECT
public:
explicit SqlProfileModel(QObject *parent = nullptr);
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
signals:
public slots:
};
#endif // SQLPROFILEMODEL_H

6
sqlstoragemodel.cpp Normal file
View file

@ -0,0 +1,6 @@
#include "sqlstoragemodel.h"
SqlStorageModel::SqlStorageModel(QObject *parent) : QObject(parent)
{
}

17
sqlstoragemodel.h Normal file
View file

@ -0,0 +1,17 @@
#ifndef SQLSTORAGEMODEL_H
#define SQLSTORAGEMODEL_H
#include <QObject>
class SqlStorageModel : public QObject
{
Q_OBJECT
public:
explicit SqlStorageModel(QObject *parent = nullptr);
signals:
public slots:
};
#endif // SQLSTORAGEMODEL_H