feat: add Training Timer and calculation of optimal Round Times
This commit is contained in:
parent
6d9151c8ec
commit
cfbd2a313b
10 changed files with 271 additions and 149 deletions
|
|
@ -2,6 +2,9 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
private val apiService by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl("http://192.168.178.43:8080/") // WICHTIG: Deine lokale IP
|
||||
.baseUrl("https://kb.patanix.de/")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(de.patani.kettlebelltracker.data.remote.ApiService::class.java)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,38 @@
|
|||
package de.patani.kettlebelltracker.data.remote
|
||||
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.Response
|
||||
|
||||
interface ApiService {
|
||||
@POST("trainings/")
|
||||
suspend fun sendTrainingData(@Body payload: TrainingPayload): Response<Unit>
|
||||
|
||||
@POST("trainings/recommend-rest")
|
||||
suspend fun getRecommendedRest(@Body request: RestRecommendationRequest): Response<ApiResponse<RestRecommendationData>>
|
||||
|
||||
}
|
||||
|
||||
data class RestRecommendationRequest(
|
||||
val uuid: String,
|
||||
val reps_per_set: Int,
|
||||
val current_sets: Int
|
||||
)
|
||||
|
||||
data class RestRecommendationResponse(
|
||||
val recommended_rest_seconds: Int
|
||||
)
|
||||
|
||||
data class ApiResponse<T>(
|
||||
val status: String,
|
||||
val message: String?,
|
||||
val data: T?
|
||||
)
|
||||
|
||||
data class RestRecommendationData(
|
||||
val recommended_rest: Int,
|
||||
val expected_sets: Int,
|
||||
val last_training_rest: Int?,
|
||||
val last_training_sets: Int?,
|
||||
val reasoning: String?
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,12 @@ package de.patani.kettlebelltracker.repositories
|
|||
import android.util.Log
|
||||
import de.patani.kettlebelltracker.data.remote.TrainingPayload
|
||||
import de.patani.kettlebelltracker.data.remote.ApiService
|
||||
import de.patani.kettlebelltracker.data.remote.RestRecommendationData
|
||||
import de.patani.kettlebelltracker.data.remote.RestRecommendationRequest
|
||||
import de.patani.kettlebelltracker.data.remote.RestRecommendationResponse
|
||||
|
||||
class ApiRepository(private val apiService: ApiService) {
|
||||
|
||||
suspend fun sendTrainingData(session: de.patani.kettlebelltracker.data.local.TrainingSession, uuid: String) {
|
||||
try {
|
||||
val rest = if (session.sets > 0) {
|
||||
|
|
@ -29,4 +33,19 @@ class ApiRepository(private val apiService: ApiService) {
|
|||
Log.e("ApiRepository", "API Error: Failed to send training data", e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getRecommendedRest(uuid: String, repsPerSet: Int, currentSets: Int): RestRecommendationData? {
|
||||
return try {
|
||||
val request = RestRecommendationRequest(uuid, repsPerSet, currentSets)
|
||||
val response = apiService.getRecommendedRest(request)
|
||||
if (response.isSuccessful) {
|
||||
response.body()?.data
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,96 +1,3 @@
|
|||
//
|
||||
//import androidx.compose.foundation.layout.*
|
||||
//import androidx.compose.foundation.lazy.LazyColumn
|
||||
//import androidx.compose.foundation.lazy.items
|
||||
//import androidx.compose.material3.*
|
||||
//import androidx.compose.runtime.*
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import de.patani.kettlebelltracker.viewmodels.HistoryViewModel
|
||||
//import de.patani.kettlebelltracker.util.formatDate
|
||||
//import de.patani.kettlebelltracker.util.formatDuration
|
||||
//import de.patani.kettlebelltracker.data.local.TrainingSession
|
||||
//
|
||||
//@Composable
|
||||
//fun HistoryScreen(viewModel: HistoryViewModel) {
|
||||
// val history by viewModel.history.collectAsState()
|
||||
//
|
||||
// if (history.isEmpty()) {
|
||||
// Box(modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) {
|
||||
// Text("Noch keine Trainingsdaten vorhanden.")
|
||||
// }
|
||||
// } else {
|
||||
// LazyColumn(
|
||||
// modifier = Modifier.fillMaxSize(),
|
||||
// contentPadding = PaddingValues(16.dp),
|
||||
// verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
// ) {
|
||||
// item {
|
||||
// Row(Modifier.fillMaxWidth()) {
|
||||
// Text("Datum", modifier = Modifier.weight(1f))
|
||||
// Text("Sätze", modifier = Modifier.weight(0.5f))
|
||||
// Text("Dauer", modifier = Modifier.weight(0.7f))
|
||||
// Text("Reps", modifier = Modifier.weight(0.5f))
|
||||
// }
|
||||
// Divider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
// }
|
||||
// items(history) { session ->
|
||||
// HistoryItem(session = session, onUpdate = viewModel::updateSession, onDelete = viewModel::deleteSession)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@OptIn(ExperimentalMaterial3Api::class)
|
||||
//@Composable
|
||||
//fun HistoryItem(session: TrainingSession, onUpdate: (TrainingSession) -> Unit, onDelete: (TrainingSession) -> Unit) {
|
||||
// var showDialog by remember { mutableStateOf(false) }
|
||||
//
|
||||
// Card(onClick = { showDialog = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
// Row(modifier = Modifier.padding(16.dp)) {
|
||||
// Text(text = session.date.formatDate(), modifier = Modifier.weight(1f))
|
||||
// Text(text = session.sets.toString(), modifier = Modifier.weight(0.5f))
|
||||
// Text(text = formatDuration(session.duration), modifier = Modifier.weight(0.7f))
|
||||
// Text(text = session.repsPerSet.toString(), modifier = Modifier.weight(0.5f))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (showDialog) {
|
||||
// EditHistoryDialog(
|
||||
// session = session,
|
||||
// onDismiss = { showDialog = false },
|
||||
// onSave = { updatedSession ->
|
||||
// onUpdate(updatedSession)
|
||||
// showDialog = false
|
||||
// },
|
||||
// onDelete = {
|
||||
// onDelete(session)
|
||||
// showDialog = false
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@Composable
|
||||
//fun EditHistoryDialog(session: TrainingSession, onDismiss: () -> Unit, onSave: (TrainingSession) -> Unit, onDelete: () -> Unit) {
|
||||
// // Implement a detailed dialog for editing if needed, similar to the Go app's dialog.
|
||||
// // For brevity, this example uses a simpler confirmation.
|
||||
// AlertDialog(
|
||||
// onDismissRequest = onDismiss,
|
||||
// title = { Text("Eintrag verwalten") },
|
||||
// text = { Text("Möchtest du diesen Eintrag löschen? Das Bearbeiten kann in einer zukünftigen Version implementiert werden.") },
|
||||
// confirmButton = {
|
||||
// Button(onClick = onDelete) {
|
||||
// Text("Löschen")
|
||||
// }
|
||||
// },
|
||||
// dismissButton = {
|
||||
// Button(onClick = onDismiss) {
|
||||
// Text("Abbrechen")
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
//}
|
||||
package de.patani.kettlebelltracker.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import de.patani.kettlebelltracker.viewmodels.TrainingViewModel
|
||||
|
|
@ -14,8 +13,8 @@ import de.patani.kettlebelltracker.util.formatDuration
|
|||
|
||||
@Composable
|
||||
fun TrainingScreen(viewModel: TrainingViewModel) {
|
||||
val state by viewModel.trainingState.collectAsState()
|
||||
val showFinishButton = state.remainingSeconds <= 0 && state.isTrainingRunning
|
||||
val trainingState by viewModel.trainingState.collectAsState()
|
||||
val showFinishButton = trainingState.remainingSeconds <= 0 && trainingState.isTrainingRunning
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -24,44 +23,65 @@ fun TrainingScreen(viewModel: TrainingViewModel) {
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Verbleibende Zeit", style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
text = formatDuration(state.remainingSeconds.toLong()),
|
||||
fontSize = 72.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
|
||||
if (trainingState.isRoundActive) {
|
||||
RoundTimerCard(
|
||||
timeRemaining = trainingState.currentRoundTime,
|
||||
totalTime = trainingState.totalRoundTime,
|
||||
)
|
||||
}
|
||||
|
||||
if (trainingState.isRoundActive) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Verbleibende Zeit", style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
text = formatDuration(trainingState.remainingSeconds.toLong()),
|
||||
fontSize = 72.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (trainingState.remainingSeconds <= 10)
|
||||
MaterialTheme.colorScheme.error
|
||||
else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Sätze", style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
text = "${state.setsDone} / ${state.goalSets}",
|
||||
text = "${trainingState.setsDone} / ${trainingState.goalSets}",
|
||||
fontSize = 60.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
Text(
|
||||
text = "${state.repsPerSet} Wiederholungen",
|
||||
text = "${trainingState.repsPerSet} Wiederholungen",
|
||||
fontSize = 24.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Button(
|
||||
onClick = viewModel::completeSet,
|
||||
enabled = state.isTrainingRunning,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)
|
||||
) {
|
||||
Text("Satz abschließen", fontSize = 18.sp)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (trainingState.isRoundActive) {
|
||||
Button(
|
||||
onClick = viewModel::completeSet,
|
||||
enabled = trainingState.isTrainingRunning,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)
|
||||
) {
|
||||
Text("Satz abschließen", fontSize = 18.sp)
|
||||
}
|
||||
}
|
||||
|
||||
if (showFinishButton) {
|
||||
Button(
|
||||
onClick = viewModel::finishTraining,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
|
|
@ -72,3 +92,82 @@ fun TrainingScreen(viewModel: TrainingViewModel) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoundTimerCard(
|
||||
timeRemaining: Int,
|
||||
totalTime: Int
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = when {
|
||||
timeRemaining <= 0 -> MaterialTheme.colorScheme.errorContainer
|
||||
timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.3f)
|
||||
else -> MaterialTheme.colorScheme.primaryContainer
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = if (timeRemaining <= 0) "Zeit abgelaufen!" else "Rundenzeit",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = when {
|
||||
timeRemaining <= 0 -> MaterialTheme.colorScheme.error
|
||||
else -> MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(120.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val progress = if (totalTime > 0 && timeRemaining >= 0) {
|
||||
(totalTime - timeRemaining).toFloat() / totalTime.toFloat()
|
||||
} else if (timeRemaining < 0) 1f else 0f
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = progress,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
strokeWidth = 8.dp,
|
||||
color = when {
|
||||
timeRemaining <= 0 -> MaterialTheme.colorScheme.error
|
||||
timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = if (timeRemaining >= 0) {
|
||||
formatDuration(timeRemaining.toLong())
|
||||
} else {
|
||||
"+${formatDuration((-timeRemaining).toLong())}"
|
||||
},
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = when {
|
||||
timeRemaining <= 0 -> MaterialTheme.colorScheme.error
|
||||
timeRemaining <= 10 -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = if (timeRemaining <= 0) "überzogen" else "verbleibend",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,9 @@
|
|||
package de.patani.kettlebelltracker.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import androidx.compose.ui.text.font.FontFamily
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
|
|
@ -15,20 +14,4 @@ val Typography = Typography(
|
|||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package de.patani.kettlebelltracker.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
|
@ -9,6 +10,7 @@ fun Date.formatDate(): String {
|
|||
return formatter.format(this)
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun formatDuration(totalSeconds: Long): String {
|
||||
if (totalSeconds < 0) return "00:00"
|
||||
val minutes = totalSeconds / 60
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue