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,
|
val weightStep: Double = 2.5,
|
||||||
/** Spara inloggningen krypterat och logga in tyst igen vid behov */
|
/** Spara inloggningen krypterat och logga in tyst igen vid behov */
|
||||||
val autoRelogin: Boolean = true,
|
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) {
|
class SettingsStore(private val context: Context) {
|
||||||
@@ -36,9 +38,14 @@ class SettingsStore(private val context: Context) {
|
|||||||
defaultRestSeconds = prefs[KEY_REST_SECONDS] ?: 90,
|
defaultRestSeconds = prefs[KEY_REST_SECONDS] ?: 90,
|
||||||
weightStep = prefs[KEY_WEIGHT_STEP]?.toDoubleOrNull() ?: 2.5,
|
weightStep = prefs[KEY_WEIGHT_STEP]?.toDoubleOrNull() ?: 2.5,
|
||||||
autoRelogin = prefs[KEY_AUTO_RELOGIN]?.toBooleanStrictOrNull() ?: true,
|
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) {
|
suspend fun setWeightStep(value: Double) {
|
||||||
context.settingsDataStore.edit { it[KEY_WEIGHT_STEP] = value.toString() }
|
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_REST_SECONDS = intPreferencesKey("default_rest_seconds")
|
||||||
private val KEY_WEIGHT_STEP = stringPreferencesKey("weight_step")
|
private val KEY_WEIGHT_STEP = stringPreferencesKey("weight_step")
|
||||||
private val KEY_AUTO_RELOGIN = stringPreferencesKey("auto_relogin")
|
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
|
package eu.brassepc.fitnessdroid.ui.session
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -71,6 +72,7 @@ fun SessionScreen(
|
|||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
var confirmComplete by remember { mutableStateOf(false) }
|
var confirmComplete by remember { mutableStateOf(false) }
|
||||||
var editTarget by remember { mutableStateOf<Pair<ExercisePage, LocalSet>?>(null) }
|
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.
|
// Passtimern (uppe till höger) tickar en gång i sekunden.
|
||||||
var nowMs by remember { mutableStateOf(System.currentTimeMillis()) }
|
var nowMs by remember { mutableStateOf(System.currentTimeMillis()) }
|
||||||
@@ -146,6 +148,7 @@ fun SessionScreen(
|
|||||||
onLog = viewModel::logSet,
|
onLog = viewModel::logSet,
|
||||||
onEditSet = { page, set -> editTarget = page to set },
|
onEditSet = { page, set -> editTarget = page to set },
|
||||||
onOpenHistory = viewModel::openExerciseHistory,
|
onOpenHistory = viewModel::openExerciseHistory,
|
||||||
|
onShowPlates = { weight -> plateWeight = weight },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
@@ -175,6 +178,14 @@ fun SessionScreen(
|
|||||||
ExerciseHistorySheet(state = sheet, onDismiss = viewModel::closeExerciseHistory)
|
ExerciseHistorySheet(state = sheet, onDismiss = viewModel::closeExerciseHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plateWeight?.let { weight ->
|
||||||
|
PlateCalculatorSheet(
|
||||||
|
weightKg = weight,
|
||||||
|
customBarWeight = uiState.settings.customBarWeight,
|
||||||
|
onDismiss = { plateWeight = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
editTarget?.let { (page, set) ->
|
editTarget?.let { (page, set) ->
|
||||||
EditSetDialog(
|
EditSetDialog(
|
||||||
page = page,
|
page = page,
|
||||||
@@ -266,6 +277,7 @@ private fun ExercisePageContent(
|
|||||||
onLog: (Long) -> Unit,
|
onLog: (Long) -> Unit,
|
||||||
onEditSet: (ExercisePage, LocalSet) -> Unit,
|
onEditSet: (ExercisePage, LocalSet) -> Unit,
|
||||||
onOpenHistory: (ExercisePage) -> Unit,
|
onOpenHistory: (ExercisePage) -> Unit,
|
||||||
|
onShowPlates: (Double) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -310,10 +322,11 @@ private fun ExercisePageContent(
|
|||||||
|
|
||||||
if (page.type?.tracksWeight != false) {
|
if (page.type?.tracksWeight != false) {
|
||||||
Stepper(
|
Stepper(
|
||||||
label = "kg · ±${weightStep.compact()}",
|
label = "kg · ±${weightStep.compact()} · tryck för skivor",
|
||||||
value = page.draft.weight?.compact() ?: "—",
|
value = page.draft.weight?.compact() ?: "—",
|
||||||
onMinus = { onDraft(page.localId) { d -> d.copy(weight = ((d.weight ?: 0.0) - weightStep).coerceAtLeast(0.0)) } },
|
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) } },
|
onPlus = { onDraft(page.localId) { d -> d.copy(weight = (d.weight ?: 0.0) + weightStep) } },
|
||||||
|
onValueClick = { page.draft.weight?.let(onShowPlates) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (page.type?.tracksReps != false) {
|
if (page.type?.tracksReps != false) {
|
||||||
@@ -435,14 +448,28 @@ private fun DoneSetRow(set: LocalSet, onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
FilledTonalIconButton(onClick = onMinus) { Text("−", style = MaterialTheme.typography.titleLarge) }
|
FilledTonalIconButton(onClick = onMinus) { Text("−", style = MaterialTheme.typography.titleLarge) }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.then(
|
||||||
|
if (onValueClick != null) {
|
||||||
|
Modifier.clickable(onClick = onValueClick)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Text(value, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
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 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 {
|
fun setAutoRelogin(value: Boolean) = viewModelScope.launch {
|
||||||
store.setAutoRelogin(value)
|
store.setAutoRelogin(value)
|
||||||
// Stängs funktionen av slängs de sparade uppgifterna direkt.
|
// 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") {
|
SettingsCard(title = "Inloggning") {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
Reference in New Issue
Block a user