Skivkalkylator: tryck på vikten i aktiva setet
All checks were successful
release / build-release (push) Successful in 5m35s
All checks were successful
release / build-release (push) Successful in 5m35s
- Ark med skivorna per sida i standardfärger (25 röd, 20 blå, 15 gul,
10 grön, 5 vit, 2,5 svart, 1,25 grå), störst innerst mot låskragen
- Stångval: 20 kg / utan stång / egen vikt (sätts i inställningarna,
standard 15 kg)
- Textsammanfattning ('Per sida: 20 + 10 + 2,5 kg') och tydlig hint om
vikten inte går att lägga exakt med standardskivor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Plate>, Int> {
|
||||
var rest = perSideGrams
|
||||
val result = mutableListOf<Plate>()
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Pair<ExercisePage, LocalSet>?>(null) }
|
||||
var plateWeight by remember { mutableStateOf<Double?>(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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user