Skivkalkylator: tryck på vikten i aktiva setet
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:
2026-07-25 17:06:10 +02:00
parent bb3502ed26
commit d577a92eec
4 changed files with 247 additions and 3 deletions

View File

@@ -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")
}
}

View File

@@ -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,
)
}
}
}
}
}

View File

@@ -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)

View File

@@ -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