kettlebell-tracker/app/src/main/java/de/patani/kettlebelltracker/viewmodels/TrainingViewModel.kt

258 lines
No EOL
8.3 KiB
Kotlin

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
import de.patani.kettlebelltracker.data.local.TrainingSessionDao
import de.patani.kettlebelltracker.data.datastore.SettingsDataStore
import de.patani.kettlebelltracker.repositories.ApiRepository
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.Date
import kotlin.math.min
data class TrainingState(
val isTrainingRunning: Boolean = false,
val remainingSeconds: Int = 0,
val initialDurationSeconds: Int = 0,
val setsDone: Int = 0,
val goalSets: Int = 5,
val repsPerSet: Int = 5,
val progress: Float = 0.0f,
val currentProgram: String = "clean_1.0",
val currentBlockDay: Int = 1,
val currentReps: Int = 5,
val totalTrainingDays: Int = 0,
// Neue Rundentimer-Eigenschaften
val isRoundActive: Boolean = false,
val currentRoundTime: Int = 0,
val totalRoundTime: Int = 0
)
class TrainingViewModel(
private val dao: TrainingSessionDao,
private val settingsDataStore: SettingsDataStore,
private val apiRepository: ApiRepository,
private val appUUID: String
) : ViewModel() {
private val _trainingState = MutableStateFlow(TrainingState())
val trainingState = _trainingState.asStateFlow()
private var timerJob: Job? = null
private var roundTimerJob: Job? = null
init {
viewModelScope.launch {
val trainingCount = dao.getTrainingCount().first()
val initialState = calculateStateByDayCount(trainingCount)
_trainingState.update {
it.copy(
totalTrainingDays = trainingCount,
currentProgram = initialState.program,
currentBlockDay = initialState.blockDay,
currentReps = initialState.reps,
repsPerSet = initialState.reps
)
}
}
}
private var recommendedRestSeconds: Int = 90
fun startTraining() {
if (_trainingState.value.isTrainingRunning) return
viewModelScope.launch {
val settings = settingsDataStore.settingsFlow.first()
val durationSeconds = settings.trainingTimeMinutes * 60
_trainingState.update {
it.copy(
isTrainingRunning = true,
initialDurationSeconds = durationSeconds,
remainingSeconds = durationSeconds,
goalSets = settings.goalSets,
setsDone = 0,
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()
}
}
private fun startTimer() {
timerJob?.cancel()
timerJob = viewModelScope.launch {
while (_trainingState.value.remainingSeconds > 0 && _trainingState.value.isTrainingRunning) {
delay(1000)
_trainingState.update { it.copy(remainingSeconds = it.remainingSeconds - 1) }
}
if (_trainingState.value.isTrainingRunning) {
finishTraining()
}
}
}
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 {
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 {
val state = _trainingState.value
val settings = settingsDataStore.settingsFlow.first()
val session = de.patani.kettlebelltracker.data.local.TrainingSession(
date = Date(),
sets = state.setsDone,
weightLeft = settings.weightLeft,
weightRight = settings.weightRight,
repsPerSet = state.repsPerSet,
duration = (state.initialDurationSeconds - state.remainingSeconds).toLong(),
program = state.currentProgram,
blockDay = state.currentBlockDay
)
dao.insert(session)
apiRepository.sendTrainingData(session, appUUID)
resetTraining()
}
}
private suspend fun resetTraining() {
val trainingCount = dao.getTrainingCount().first()
val nextState = calculateStateByDayCount(trainingCount)
_trainingState.value = TrainingState(
totalTrainingDays = trainingCount,
currentProgram = nextState.program,
currentBlockDay = nextState.blockDay,
currentReps = nextState.reps,
repsPerSet = nextState.reps
)
}
private fun calculateStateByDayCount(totalDays: Int): ProgramState {
if (totalDays <= 0) {
return ProgramState("clean_1.0", 1, 5)
}
val cycleIndex = (totalDays / 12) % 6
val programs = listOf("clean_1.0", "snatch_1.0", "clean_1.1", "snatch_1.1", "clean_1.2", "snatch_1.2")
val program = programs[cycleIndex]
val blockDay = (totalDays % 3) + 1
val repsMap = mapOf(
"clean_1.0" to listOf(5, 6, 4),
"clean_1.1" to listOf(6, 8, 7),
"clean_1.2" to listOf(7, 9, 8),
"snatch_1.0" to listOf(5, 6, 4),
"snatch_1.1" to listOf(6, 8, 7),
"snatch_1.2" to listOf(7, 9, 8)
)
val reps = repsMap[program]?.getOrNull(blockDay - 1) ?: 5
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)
}