258 lines
No EOL
8.3 KiB
Kotlin
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)
|
|
} |