feat: add Training Timer and calculation of optimal Round Times

This commit is contained in:
Patryk Hegenberg 2025-09-22 19:29:48 +02:00
parent 6d9151c8ec
commit cfbd2a313b
10 changed files with 271 additions and 149 deletions

View file

@ -1,5 +1,7 @@
package de.patani.kettlebelltracker.viewmodels
import android.media.AudioManager
import android.media.ToneGenerator
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -27,7 +29,11 @@ data class TrainingState(
val currentProgram: String = "clean_1.0",
val currentBlockDay: Int = 1,
val currentReps: Int = 5,
val totalTrainingDays: Int = 0
val totalTrainingDays: Int = 0,
// Neue Rundentimer-Eigenschaften
val isRoundActive: Boolean = false,
val currentRoundTime: Int = 0,
val totalRoundTime: Int = 0
)
class TrainingViewModel(
@ -41,9 +47,9 @@ class TrainingViewModel(
val trainingState = _trainingState.asStateFlow()
private var timerJob: Job? = null
private var roundTimerJob: Job? = null
init {
// Load initial state based on past trainings
viewModelScope.launch {
val trainingCount = dao.getTrainingCount().first()
val initialState = calculateStateByDayCount(trainingCount)
@ -59,6 +65,8 @@ class TrainingViewModel(
}
}
private var recommendedRestSeconds: Int = 90
fun startTraining() {
if (_trainingState.value.isTrainingRunning) return
@ -76,7 +84,15 @@ class TrainingViewModel(
progress = 0.0f
)
}
val recommendation = apiRepository.getRecommendedRest(
uuid = appUUID,
repsPerSet = _trainingState.value.repsPerSet,
currentSets = 0 // ggf. 0 oder deinen Startwert
)
recommendedRestSeconds = recommendation?.recommended_rest ?: 90
Log.d("Training", "using rest time ${recommendedRestSeconds.toString()}")
startTimer()
startFirstRound()
}
}
@ -93,20 +109,87 @@ class TrainingViewModel(
}
}
private fun startFirstRound() {
startRoundTimer(recommendedRestSeconds)
}
fun completeSet() {
if (!_trainingState.value.isTrainingRunning) return
val currentState = _trainingState.value
val newSetsDone = currentState.setsDone + 1
val newProgress = if (currentState.goalSets > 0) {
min(newSetsDone.toFloat() / currentState.goalSets.toFloat(), 1.0f)
} else 0.0f
_trainingState.update {
val newSetsDone = it.setsDone + 1
val newProgress = if (it.goalSets > 0) {
min(newSetsDone.toFloat() / it.goalSets.toFloat(), 1.0f)
} else 0.0f
it.copy(setsDone = newSetsDone, progress = newProgress)
it.copy(
setsDone = newSetsDone,
progress = newProgress
)
}
stopRoundTimer()
if (newSetsDone < currentState.goalSets) {
startNextRound()
}
}
private fun startNextRound() {
startRoundTimer(recommendedRestSeconds)
}
private fun startRoundTimer(seconds: Int) {
roundTimerJob?.cancel()
_trainingState.update {
it.copy(
isRoundActive = true,
currentRoundTime = seconds,
totalRoundTime = seconds
)
}
roundTimerJob = viewModelScope.launch {
while (_trainingState.value.currentRoundTime > 0 && _trainingState.value.isRoundActive) {
delay(1000)
_trainingState.update {
it.copy(currentRoundTime = it.currentRoundTime - 1)
}
}
if (_trainingState.value.isRoundActive) {
playRoundCompleteSound()
}
}
}
private fun stopRoundTimer() {
roundTimerJob?.cancel()
_trainingState.update {
it.copy(
isRoundActive = false,
currentRoundTime = 0,
totalRoundTime = 0
)
}
}
private fun playRoundCompleteSound() {
viewModelScope.launch {
try {
val toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100)
toneGenerator.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 1000)
} catch (e: Exception) {
Log.e("TrainingViewModel", "Could not play sound", e)
}
}
}
fun finishTraining() {
timerJob?.cancel()
roundTimerJob?.cancel()
if (!_trainingState.value.isTrainingRunning) return
viewModelScope.launch {
@ -165,5 +248,11 @@ class TrainingViewModel(
return ProgramState(program, blockDay, reps)
}
override fun onCleared() {
super.onCleared()
timerJob?.cancel()
roundTimerJob?.cancel()
}
data class ProgramState(val program: String, val blockDay: Int, val reps: Int)
}