2019-04-06 20:16:16 +02:00
/ *
Speed climbing reaction timer
Copyright ( C ) 2019 Itsblue Development | Dorian Zedler
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 , either version 3 of the License , or
( at your option ) any later version .
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 < http: //www.gnu.org/licenses/>.
* /
2019-04-06 20:09:40 +02:00
import QtQuick 2.9
import QtQuick . Window 2.2
2020-08-13 23:53:29 +02:00
import QtQuick . Controls 2.0
2023-01-18 14:35:25 +01:00
import QtMultimedia 5.15
2019-04-06 20:09:40 +02:00
import QtGraphicalEffects 1.0
2023-01-18 14:35:25 +01:00
import QtQuick . Controls . Material 2.15
2019-04-06 20:09:40 +02:00
import com . itsblue . SpeedClimbingStartTrainer 2.0
import "./Components"
Window {
visible: true
width: 640
height: 480
2019-09-22 16:12:43 +02:00
title: qsTr ( "Speed climbing reaction trainer" )
2019-04-06 20:09:40 +02:00
Page {
id: app
state: "IDLE"
anchors.fill: parent
property double startTime: - 1
property double stopTime: - 1
property double reactionTime: 0
property int highscore: settings . read ( "highscore" )
AppSettings {
id: settings
}
AppTheme {
id: theme
}
Item {
id: soundsItm
2023-01-18 14:35:25 +01:00
SoundEffect {
2019-04-06 20:09:40 +02:00
id: readySe
source: "qrc:/sounds/ready_1.wav"
2023-01-18 14:35:25 +01:00
onPlayingChanged: {
if ( playing ) return
2019-04-06 20:09:40 +02:00
2019-10-13 16:46:49 +02:00
if ( app . state === "RUNNING" ) {
2019-04-06 20:09:40 +02:00
startSe . play ( )
}
}
}
2023-01-18 14:35:25 +01:00
SoundEffect {
2019-04-06 20:09:40 +02:00
id: startSe
2019-09-22 16:12:43 +02:00
source: "qrc:/sounds/IFSC_STARTSIGNAL_SINE.wav"
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
onPlayingChanged: {
if ( ! playing ) return
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
app . startTime = new Date ( ) . getTime ( )
}
2019-10-13 16:46:49 +02:00
2019-04-06 20:09:40 +02:00
}
2023-01-18 14:35:25 +01:00
SoundEffect {
2019-04-06 20:09:40 +02:00
id: failedSe
2019-10-13 16:46:49 +02:00
2019-09-22 16:12:43 +02:00
source: "qrc:/sounds/IFSC frequenzy conform false start sound.wav"
2019-04-06 20:09:40 +02:00
}
}
Rectangle {
anchors.fill: parent
color: theme . style . backgroundColor
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
Item {
id: topContainerItm
anchors {
top: parent . top
left: parent . left
right: app . landscape ( ) ? startBt.left: parent . right
bottom: app . landscape ( ) ? parent.bottom: startBt . top
bottomMargin: app . landscape ( ) ? undefined: parent . height * 0.1
rightMargin: app . landscape ( ) ? parent . width * 0.05 : 0
}
Text {
id: topLa
anchors.centerIn: parent
width: parent . width * 0.8
text: ""
color: theme . style . textColor
fontSizeMode: Text . Fit
verticalAlignment: Text . AlignVCenter
horizontalAlignment: Text . AlignHCenter
font.pixelSize: app . landscape ( ) ? parent . width * 0.1 : parent . height * 0.4
minimumPixelSize: 0
Behavior on text {
FadeAnimation {
target: topLa
}
}
}
Text {
id: highscoreLa
anchors {
top: topLa . bottom
horizontalCenter: parent . horizontalCenter
}
width: topLa . width * 0.5
height: topLa . height
visible: text !== "highscore: -1"
text: ""
color: theme . style . textColor
fontSizeMode: Text . Fit
verticalAlignment: Text . AlignVCenter
horizontalAlignment: Text . AlignHCenter
font.pixelSize: app . landscape ( ) ? parent . width * 0.1 : parent . height * 0.4
minimumPixelSize: 0
Behavior on text {
FadeAnimation {
target: highscoreLa
}
}
}
2023-01-18 14:35:25 +01:00
Row {
anchors {
top: topLa . bottom
horizontalCenter: parent . horizontalCenter
horizontalCenterOffset: width * 0.1
}
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
opacity: app . state === "IDLE" ? 1 : 0
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
Switch {
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
anchors.verticalCenter: parent . verticalCenter
checked: settings . read ( "theme" ) === "1"
scale: 0.7
2019-04-06 20:09:40 +02:00
onCheckedChanged: {
if ( checked ) {
settings . write ( "theme" , 1 )
}
else {
settings . write ( "theme" , 0 )
}
theme . refreshTheme ( )
}
indicator: Rectangle {
property bool checked: parent . checked
property bool down: parent . down
height: parent . height
width: height * 2
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 ? theme.style.buttonPressedColor: theme . style . backgroundColor //"#cccccc" : "#ffffff"
border.color: parent . checked ? ( parent . down ? "#17a81a" : "#21be2b" ) : "#999999"
Behavior on x {
NumberAnimation {
property: "x"
duration: 200
easing.type: Easing . InOutQuad
}
}
}
}
2023-01-18 14:35:25 +01:00
}
ToolButton {
anchors.verticalCenter: parent . verticalCenter
bottomPadding: 0
topPadding: 0
text: "\uf05a"
scale: 1.2
font.family: fontAwesome . name
font.pixelSize: height * 0.4
Material.theme: theme . style . name === "dark" ? Material.Dark: Material . Light
onClicked: infoDisclaimerDialog . open ( )
}
Behavior on opacity {
NumberAnimation {
duration: 200
2019-04-06 20:09:40 +02:00
}
2023-01-18 14:35:25 +01:00
}
2019-04-06 20:09:40 +02:00
}
2023-01-18 14:35:25 +01:00
2019-04-06 20:09:40 +02:00
}
FancyButton {
id: startBt
text: "start"
property int size: app . landscape ( ) ? parent . width * 0.5 : parent . height * 0.5
backgroundColor: theme . style . buttonColor
textColor: theme . style . textColor
anchors {
bottom: parent . bottom
right: parent . right
bottomMargin: app . landscape ( ) ? parent . height * 0.5 - startBt . height * 0.5 : parent . height * 0.1
rightMargin: app . landscape ( ) ? parent . width * 0.05 : parent . width * 0.5 - startBt . width * 0.5
}
font.pixelSize: height * 0.15
height: app . landscape ( ) ? ( size > parent . height * 0.9 ? parent . height * 0.9 : size ) : ( size > parent . width * 0.9 ? parent . width * 0.9 : size )
width: height
onPressed: {
app . start ( )
}
onReleased: {
app . stop ( )
}
}
function landscape ( ) {
return app . height < app . width
}
function start ( ) {
if ( app . state !== "IDLE" ) {
app . reset ( )
return
}
app . state = "RUNNING"
readySe . play ( )
}
function stop ( ) {
app . stopTime = new Date ( ) . getTime ( )
if ( app . state !== "RUNNING" ) {
return
}
2023-01-18 14:35:25 +01:00
if ( readySe . playing ) {
2019-04-06 20:09:40 +02:00
app . state = "IDLE"
2023-01-18 14:35:25 +01:00
return
2019-04-06 20:09:40 +02:00
}
2023-01-18 14:35:25 +01:00
app . state = "STOPPED"
startSe . stop ( )
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
app . reactionTime = app . stopTime - app . startTime - 3000
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
if ( app . reactionTime < 100 ) {
failedSe . play ( )
2019-04-06 20:09:40 +02:00
}
2023-01-18 14:35:25 +01:00
app . refreshHighscore ( )
2019-04-06 20:09:40 +02:00
}
function refreshHighscore ( ) {
2023-01-18 14:35:25 +01:00
const currentHighscore = parseInt ( settings . read ( "highscore" ) )
// migrate old Highscores
if ( currentHighscore < 100 ) {
settings . write ( "highscore" , currentHighscore + 100 )
app . highscore = currentHighscore + 100
}
if ( app . reactionTime > 100 && ( app . reactionTime < currentHighscore || settings . read ( "highscore" ) === "-1" ) ) {
2019-04-06 20:09:40 +02:00
settings . write ( "highscore" , app . reactionTime )
app . highscore = app . reactionTime
}
}
function reset ( ) {
app . state = "IDLE"
2023-01-18 14:35:25 +01:00
app . startTime = 0
2019-04-06 20:09:40 +02:00
}
states: [
State {
name: "IDLE"
PropertyChanges {
target: topLa
text: "Hold down start to start"
}
} ,
State {
name: "RUNNING"
PropertyChanges {
target: topLa
text: "release to stop"
}
} ,
State {
name: "STOPPED"
PropertyChanges {
target: topLa
text: "Your reaction time: " + app . reactionTime
}
PropertyChanges {
target: startBt
text: "reset"
}
PropertyChanges {
target: highscoreLa
text: "highscore: " + app . highscore
}
2023-01-18 14:35:25 +01:00
}
]
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
FontLoader {
id: fontAwesome
source: "qrc:/fonts/fa5solid.woff"
2019-04-06 20:09:40 +02:00
2023-01-18 14:35:25 +01:00
Component.onCompleted: {
console . log ( "Font name: " + fontAwesome . name )
2019-04-06 20:09:40 +02:00
}
2023-01-18 14:35:25 +01:00
}
DisclaimerDialog {
id: infoDisclaimerDialog
Material.theme: theme . style . name === "dark" ? Material.Dark: Material . Light
title: "Speedclimbing reaction trainer v" + APP_VERSION + "<br>By <a href=\"https://itsblue.de\">Itsblue Development</a>, <a href=\"https://itsblue.de/apps/scrt\">" + qsTr ( "privacy policy" ) + "</a>"
content: qsTr ( "This app was built using the <a href='https://qt.io'>Qt Framework</a> licensed under the <a href='https://www.gnu.org/licenses/lgpl-3.0.en.html'>GNU lgplV3 license</a>.<br><br>This app is open source and licensed under the <a href='https://www.gnu.org/licenses/agpl-3.0.en.html'>GNU agplV3 license</a>, the source code can be found <a href='https://itsblue.dev/ScStw/reaction-trainer'>here</a>." )
}
2019-04-06 20:09:40 +02:00
}
}