diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/data/SettingsStore.kt b/app/src/main/java/eu/brassepc/fitnessdroid/data/SettingsStore.kt index 36a6b8f..305f2e5 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/SettingsStore.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/SettingsStore.kt @@ -21,6 +21,8 @@ data class AppSettings( val weightStep: Double = 2.5, /** Spara inloggningen krypterat och logga in tyst igen vid behov */ val autoRelogin: Boolean = true, + /** Egen stångvikt i skivkalkylatorn (utöver snabbvalen 20/0 kg) */ + val customBarWeight: Double = 15.0, ) class SettingsStore(private val context: Context) { @@ -36,9 +38,14 @@ class SettingsStore(private val context: Context) { defaultRestSeconds = prefs[KEY_REST_SECONDS] ?: 90, weightStep = prefs[KEY_WEIGHT_STEP]?.toDoubleOrNull() ?: 2.5, autoRelogin = prefs[KEY_AUTO_RELOGIN]?.toBooleanStrictOrNull() ?: true, + customBarWeight = prefs[KEY_CUSTOM_BAR]?.toDoubleOrNull() ?: 15.0, ) } + suspend fun setCustomBarWeight(value: Double) { + context.settingsDataStore.edit { it[KEY_CUSTOM_BAR] = value.coerceIn(0.0, 60.0).toString() } + } + suspend fun setWeightStep(value: Double) { context.settingsDataStore.edit { it[KEY_WEIGHT_STEP] = value.toString() } } @@ -65,5 +72,6 @@ class SettingsStore(private val context: Context) { private val KEY_REST_SECONDS = intPreferencesKey("default_rest_seconds") private val KEY_WEIGHT_STEP = stringPreferencesKey("weight_step") private val KEY_AUTO_RELOGIN = stringPreferencesKey("auto_relogin") + private val KEY_CUSTOM_BAR = stringPreferencesKey("custom_bar_weight") } } diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/PlateCalculator.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/PlateCalculator.kt new file mode 100644 index 0000000..51e7a56 --- /dev/null +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/PlateCalculator.kt @@ -0,0 +1,178 @@ +package eu.brassepc.fitnessdroid.ui.session + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** Standardfärger på viktskivor (IPF-ish). Vikt i gram för exakt matematik. */ +private data class Plate(val grams: Int, val color: Color, val heightDp: Dp, val widthDp: Dp) + +private val PLATES = listOf( + Plate(25_000, Color(0xFFD32F2F), 88.dp, 13.dp), // 25 röd + Plate(20_000, Color(0xFF1E64C8), 88.dp, 12.dp), // 20 blå + Plate(15_000, Color(0xFFF9C513), 76.dp, 11.dp), // 15 gul + Plate(10_000, Color(0xFF3E9B4F), 62.dp, 10.dp), // 10 grön + Plate(5_000, Color(0xFFECECE4), 46.dp, 9.dp), // 5 vit + Plate(2_500, Color(0xFF212121), 34.dp, 8.dp), // 2,5 svart + Plate(1_250, Color(0xFF9E9E9E), 26.dp, 7.dp), // 1,25 grå +) + +private fun breakdown(perSideGrams: Int): Pair, Int> { + var rest = perSideGrams + val result = mutableListOf() + for (plate in PLATES) { + while (rest >= plate.grams) { + result += plate + rest -= plate.grams + } + } + return result to rest +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PlateCalculatorSheet( + weightKg: Double, + customBarWeight: Double, + onDismiss: () -> Unit, +) { + var barKg by remember { mutableStateOf(20.0) } + val barOptions = buildList { + add(20.0) + add(0.0) + if (customBarWeight != 20.0 && customBarWeight != 0.0) add(customBarWeight) + } + + val perSideGrams = (((weightKg - barKg) / 2.0) * 1000).toInt().coerceAtLeast(0) + val (plates, restGrams) = breakdown(perSideGrams) + + ModalBottomSheet(onDismissRequest = onDismiss) { + Column( + modifier = Modifier.padding(horizontal = 20.dp).padding(bottom = 28.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + "${weightKg.compact()} kg på stången", + style = MaterialTheme.typography.titleLarge, + ) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + barOptions.forEach { option -> + FilterChip( + selected = barKg == option, + onClick = { barKg = option }, + label = { + Text( + when (option) { + 0.0 -> "Utan stång" + customBarWeight -> "Egen ${option.compact()} kg" + else -> "Stång ${option.compact()} kg" + } + ) + }, + ) + } + } + + if (weightKg < barKg) { + Text( + "Vikten är lägre än stången — bara stången räcker (${barKg.compact()} kg).", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + // Skivorna för ena sidan, störst innerst + Row( + modifier = Modifier + .fillMaxWidth() + .height(96.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // stångskaft in mot mitten + Box( + modifier = Modifier + .weight(1f) + .height(8.dp) + .background(Color(0xFF757575), RoundedCornerShape(3.dp)), + ) + // låskrage + Box( + modifier = Modifier + .padding(horizontal = 2.dp) + .size(width = 8.dp, height = 24.dp) + .background(Color(0xFF8D8D8D), RoundedCornerShape(2.dp)), + ) + plates.forEach { plate -> + Box( + modifier = Modifier + .padding(horizontal = 1.5.dp) + .size(width = plate.widthDp, height = plate.heightDp) + .background(plate.color, RoundedCornerShape(3.dp)), + ) + } + Box( + modifier = Modifier + .weight(0.3f) + .height(8.dp) + .background(Color(0xFF757575), RoundedCornerShape(3.dp)), + ) + } + + Text( + buildString { + append("Per sida: ") + if (plates.isEmpty()) { + append("inga skivor") + } else { + append( + plates.groupBy { it.grams }.entries + .sortedByDescending { it.key } + .joinToString(" + ") { (grams, list) -> + val kg = grams / 1000.0 + if (list.size > 1) "${list.size}×${kg.compact()}" else kg.compact() + } + ) + append(" kg") + } + }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + + if (restGrams > 0) { + val reachable = weightKg - (restGrams * 2 / 1000.0) + Text( + "Går inte att lägga exakt med standardskivor — närmast är " + + "${reachable.compact()} kg (${(restGrams / 1000.0).compact()} kg/sida saknas).", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } +} diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionScreen.kt index e25bb1b..f741888 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionScreen.kt @@ -1,6 +1,7 @@ package eu.brassepc.fitnessdroid.ui.session import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -71,6 +72,7 @@ fun SessionScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() var confirmComplete by remember { mutableStateOf(false) } var editTarget by remember { mutableStateOf?>(null) } + var plateWeight by remember { mutableStateOf(null) } // Passtimern (uppe till höger) tickar en gång i sekunden. var nowMs by remember { mutableStateOf(System.currentTimeMillis()) } @@ -146,6 +148,7 @@ fun SessionScreen( onLog = viewModel::logSet, onEditSet = { page, set -> editTarget = page to set }, onOpenHistory = viewModel::openExerciseHistory, + onShowPlates = { weight -> plateWeight = weight }, ) } FilledTonalButton( @@ -175,6 +178,14 @@ fun SessionScreen( ExerciseHistorySheet(state = sheet, onDismiss = viewModel::closeExerciseHistory) } + plateWeight?.let { weight -> + PlateCalculatorSheet( + weightKg = weight, + customBarWeight = uiState.settings.customBarWeight, + onDismiss = { plateWeight = null }, + ) + } + editTarget?.let { (page, set) -> EditSetDialog( page = page, @@ -266,6 +277,7 @@ private fun ExercisePageContent( onLog: (Long) -> Unit, onEditSet: (ExercisePage, LocalSet) -> Unit, onOpenHistory: (ExercisePage) -> Unit, + onShowPlates: (Double) -> Unit, ) { Column( modifier = Modifier @@ -310,10 +322,11 @@ private fun ExercisePageContent( if (page.type?.tracksWeight != false) { Stepper( - label = "kg · ±${weightStep.compact()}", + label = "kg · ±${weightStep.compact()} · tryck för skivor", value = page.draft.weight?.compact() ?: "—", onMinus = { onDraft(page.localId) { d -> d.copy(weight = ((d.weight ?: 0.0) - weightStep).coerceAtLeast(0.0)) } }, onPlus = { onDraft(page.localId) { d -> d.copy(weight = (d.weight ?: 0.0) + weightStep) } }, + onValueClick = { page.draft.weight?.let(onShowPlates) }, ) } if (page.type?.tracksReps != false) { @@ -435,14 +448,28 @@ private fun DoneSetRow(set: LocalSet, onClick: () -> Unit) { } @Composable -private fun Stepper(label: String, value: String, onMinus: () -> Unit, onPlus: () -> Unit) { +private fun Stepper( + label: String, + value: String, + onMinus: () -> Unit, + onPlus: () -> Unit, + onValueClick: (() -> Unit)? = null, +) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { FilledTonalIconButton(onClick = onMinus) { Text("−", style = MaterialTheme.typography.titleLarge) } Column( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .then( + if (onValueClick != null) { + Modifier.clickable(onClick = onValueClick) + } else { + Modifier + } + ), horizontalAlignment = Alignment.CenterHorizontally, ) { Text(value, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt index be82d2f..22dafb0 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt @@ -65,6 +65,10 @@ class SettingsViewModel( fun setWeightStep(value: Double) = viewModelScope.launch { store.setWeightStep(value) } + fun adjustCustomBar(delta: Double) = viewModelScope.launch { + store.setCustomBarWeight(settings.value.customBarWeight + delta) + } + fun setAutoRelogin(value: Boolean) = viewModelScope.launch { store.setAutoRelogin(value) // Stängs funktionen av slängs de sparade uppgifterna direkt. @@ -177,6 +181,33 @@ fun SettingsScreen( } } + SettingsCard(title = "Skivkalkylatorn — egen stångvikt") { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + FilledTonalButton(onClick = { viewModel.adjustCustomBar(-2.5) }) { Text("−2,5") } + Text( + "${if (settings.customBarWeight % 1.0 == 0.0) settings.customBarWeight.toInt().toString() else settings.customBarWeight} kg", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.weight(1f), + textAlign = androidx.compose.ui.text.style.TextAlign.Center, + ) + FilledTonalButton(onClick = { viewModel.adjustCustomBar(2.5) }) { Text("+2,5") } + } + Text( + "Snabbvalen 20 kg och utan stång finns alltid i kalkylatorn — " + + "det här är det tredje alternativet (t.ex. lättare stång).", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp), + ) + } + SettingsCard(title = "Inloggning") { Row( modifier = Modifier