This commit is contained in:
2026-07-27 12:25:15 +02:00
parent e905ec5bb8
commit 56b1f4263b
3 changed files with 164 additions and 221 deletions
@@ -1,7 +1,8 @@
package de.drazz.boehmitools.training.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -9,6 +10,7 @@ import androidx.compose.material.icons.filled.FitnessCenter
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
@@ -19,6 +21,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.NavHost
@@ -48,16 +51,19 @@ fun AppRoot(
val backStack by navController.currentBackStackEntryAsState()
Scaffold(
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.background,
containerColor = MaterialTheme.colorScheme.background,
bottomBar = {
Box(Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp)) {
Box(Modifier.padding(horizontal = 18.dp, vertical = 12.dp)) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
tonalElevation = 10.dp,
shadowElevation = 12.dp,
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.surface,
shadowElevation = 14.dp,
tonalElevation = 4.dp,
) {
NavigationBar(containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface) {
NavigationBar(
containerColor = Color.Transparent,
tonalElevation = 0.dp,
) {
destinations.forEach { destination ->
val selected = backStack?.destination?.hierarchy?.any { it.route == destination.route } == true
NavigationBarItem(
@@ -72,7 +78,9 @@ fun AppRoot(
icon = destination.icon,
label = { Text(destination.label) },
colors = NavigationBarItemDefaults.colors(
indicatorColor = androidx.compose.material3.MaterialTheme.colorScheme.primaryContainer,
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
selectedIconColor = MaterialTheme.colorScheme.primary,
selectedTextColor = MaterialTheme.colorScheme.primary,
),
)
}
@@ -81,7 +89,7 @@ fun AppRoot(
}
},
) { padding ->
NavHost(navController = navController, startDestination = "tracker") {
NavHost(navController = navController, startDestination = "tracker", modifier = Modifier.fillMaxSize()) {
composable("tracker") { TrackerScreen(contentPadding = padding) }
composable("settings") {
SettingsScreen(
@@ -102,7 +110,7 @@ private fun WhatsNewDialog(onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Neu in RipMe ${BuildConfig.VERSION_NAME}") },
text = { Text("Modern Card Design, klarere Trainingseingaben und eine kompaktere Navigation.") },
text = { Text("Modern Card UI, sicherer Offline-Tracker und vollständige Analyseansichten.") },
confirmButton = { TextButton(onClick = onDismiss) { Text("Verstanden") } },
)
}
@@ -2,64 +2,45 @@ package de.drazz.boehmitools.training.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val ModernPurple = Color(0xFF7C3AED)
val ModernViolet = Color(0xFF9B5CF6)
val ModernIndigo = Color(0xFF4F46E5)
val ModernGreen = Color(0xFF22C55E)
private val Light = lightColorScheme(
primary = ModernPurple,
primary = Color(0xFF7C3AED),
onPrimary = Color.White,
primaryContainer = Color(0xFFEDE9FE),
onPrimaryContainer = Color(0xFF32136B),
secondary = ModernIndigo,
onPrimaryContainer = Color(0xFF2E1065),
secondary = Color(0xFF4F46E5),
onSecondary = Color.White,
secondaryContainer = Color(0xFFE0E7FF),
onSecondaryContainer = Color(0xFF1E1B4B),
tertiary = ModernGreen,
background = Color(0xFFF6F5FA),
onBackground = Color(0xFF17151D),
tertiary = Color(0xFF06B6D4),
background = Color(0xFFF4F5FB),
onBackground = Color(0xFF171725),
surface = Color(0xFFFFFFFF),
onSurface = Color(0xFF17151D),
surfaceVariant = Color(0xFFF0EEF5),
onSurfaceVariant = Color(0xFF66616F),
outline = Color(0xFFDDD8E7),
outlineVariant = Color(0xFFEAE6F0),
onSurface = Color(0xFF171725),
surfaceVariant = Color(0xFFF0F1F7),
onSurfaceVariant = Color(0xFF69697A),
outline = Color(0xFFD9DAE5),
)
private val Dark = darkColorScheme(
primary = Color(0xFFA78BFA),
onPrimary = Color(0xFF221047),
onPrimary = Color(0xFF24005B),
primaryContainer = Color(0xFF4C1D95),
onPrimaryContainer = Color(0xFFF3E8FF),
onPrimaryContainer = Color(0xFFEDE9FE),
secondary = Color(0xFF818CF8),
onSecondary = Color(0xFF11133F),
onSecondary = Color(0xFF10144A),
secondaryContainer = Color(0xFF312E81),
onSecondaryContainer = Color(0xFFE0E7FF),
tertiary = Color(0xFF34D399),
background = Color(0xFF0D111A),
onBackground = Color(0xFFF5F3FA),
surface = Color(0xFF151A25),
onSurface = Color(0xFFF5F3FA),
surfaceVariant = Color(0xFF202634),
onSurfaceVariant = Color(0xFFB7B2C3),
outline = Color(0xFF383F50),
outlineVariant = Color(0xFF282E3B),
)
private val ModernTypography = Typography().copy(
headlineSmall = Typography().headlineSmall.copy(fontWeight = FontWeight.Bold, letterSpacing = (-0.4).sp),
titleLarge = Typography().titleLarge.copy(fontWeight = FontWeight.Bold),
titleMedium = Typography().titleMedium.copy(fontWeight = FontWeight.SemiBold),
labelLarge = Typography().labelLarge.copy(fontWeight = FontWeight.SemiBold),
tertiary = Color(0xFF22D3EE),
background = Color(0xFF0B0D17),
onBackground = Color(0xFFF5F5FA),
surface = Color(0xFF151824),
onSurface = Color(0xFFF5F5FA),
surfaceVariant = Color(0xFF202432),
onSurfaceVariant = Color(0xFFB8BBC8),
outline = Color(0xFF343849),
)
@Composable
@@ -69,9 +50,5 @@ fun TrainingTheme(theme: String, content: @Composable () -> Unit) {
"light" -> false
else -> isSystemInDarkTheme()
}
MaterialTheme(
colorScheme = if (dark) Dark else Light,
typography = ModernTypography,
content = content,
)
MaterialTheme(colorScheme = if (dark) Dark else Light, content = content)
}
@@ -11,15 +11,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.MoreVert
@@ -30,7 +26,6 @@ import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
@@ -45,24 +40,21 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import de.drazz.boehmitools.training.ui.theme.ModernIndigo
import de.drazz.boehmitools.training.ui.theme.ModernPurple
import de.drazz.boehmitools.training.ui.theme.ModernViolet
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.drazz.boehmitools.training.data.local.PendingMutation
@@ -100,50 +92,71 @@ fun TrackerScreen(
val pending by viewModel.pending.collectAsStateWithLifecycle()
val currentPending = pending.filter { it.planId == state.selectedPlanId }
var planMenu by remember { mutableStateOf(false) }
val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ")
Scaffold(
modifier = Modifier.padding(contentPadding),
containerColor = MaterialTheme.colorScheme.background,
topBar = {
TopAppBar(
title = {
Column {
Text(state.plan?.title ?: "RipMe", maxLines = 2)
if (state.saveState.isNotBlank()) Text(state.saveState, style = MaterialTheme.typography.labelSmall)
Column(
Modifier
.fillMaxSize()
.padding(contentPadding)
.background(MaterialTheme.colorScheme.background),
) {
Row(
Modifier.fillMaxWidth().padding(start = 20.dp, end = 12.dp, top = 16.dp, bottom = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(Modifier.weight(1f)) {
Text("TRAININGSPLAN", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
Text(
state.plan?.title ?: "RipMe",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
maxLines = 2,
)
if (state.saveState.isNotBlank()) {
Text(state.saveState, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
Box {
IconButton(onClick = { planMenu = true }) { Icon(Icons.Default.MoreVert, contentDescription = "Plan wechseln") }
DropdownMenu(expanded = planMenu, onDismissRequest = { planMenu = false }) {
state.plans.forEach { plan ->
DropdownMenuItem(text = { Text(plan.title) }, onClick = { planMenu = false; viewModel.selectPlan(plan.id) })
}
},
actions = {
Box {
IconButton(onClick = { planMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "Plan wechseln")
}
DropdownMenu(expanded = planMenu, onDismissRequest = { planMenu = false }) {
state.plans.forEach { plan ->
DropdownMenuItem(
text = { Text(plan.title) },
onClick = { planMenu = false; viewModel.selectPlan(plan.id) },
)
}
}
}
},
)
},
) { inner ->
Column(Modifier.fillMaxSize().padding(inner)) {
val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ")
PrimaryTabRow(
selectedTabIndex = tabs.indexOfFirst { it.first == state.tab }.coerceAtLeast(0),
containerColor = MaterialTheme.colorScheme.background,
) {
tabs.forEach { (id, label) ->
Tab(
selected = state.tab == id,
onClick = { viewModel.setTab(id) },
text = { Text(label, maxLines = 2, style = MaterialTheme.typography.labelMedium) },
}
}
}
Row(
Modifier
.padding(horizontal = 16.dp, vertical = 4.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(18.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
tabs.forEach { (id, label) ->
val selected = state.tab == id
Surface(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(14.dp),
color = if (selected) MaterialTheme.colorScheme.surface else Color.Transparent,
shadowElevation = if (selected) 3.dp else 0.dp,
onClick = { viewModel.setTab(id) },
) {
Text(
label,
modifier = Modifier.padding(vertical = 10.dp),
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
style = MaterialTheme.typography.labelMedium,
fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
Box(Modifier.fillMaxSize()) {
when {
state.loading && state.plan == null -> Box(Modifier.fillMaxSize().padding(32.dp)) { CircularProgressIndicator() }
state.error.isNotBlank() && state.plan == null -> ErrorPane(state.error) { viewModel.loadPlans(force = true) }
@@ -162,7 +175,7 @@ fun TrackerScreen(
AlertDialog(
onDismissRequest = viewModel::dismissConflict,
title = { Text("Revisionskonflikt") },
text = { Text("Der Server steht bereits auf Revision ${conflict.currentRevision}. Deine lokale Änderung wird nicht automatisch überschrieben. Lade den Serverstand neu und wende die Änderung bewusst erneut an.") },
text = { Text("Der Server steht bereits auf Revision ${conflict.currentRevision}. Deine lokale Änderung wird nicht automatisch überschrieben.") },
confirmButton = { TextButton(onClick = viewModel::reloadAfterConflict) { Text("Serverstand neu laden") } },
dismissButton = { TextButton(onClick = viewModel::dismissConflict) { Text("Lokale Ansicht behalten") } },
)
@@ -183,33 +196,24 @@ private fun SessionPane(state: TrackerUiState, pending: List<PendingMutation>, v
val day = plan.days.firstOrNull { it.num == state.day } ?: return
val session = trackerSession(state.tracker, de.drazz.boehmitools.training.domain.sessionKey(state.week, state.day), plan)
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp),
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp, vertical = 14.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error)
if (pending.isNotEmpty()) PendingQueue(pending, vm)
WeekDayPicker(plan, state.tracker, state.week, state.day, vm)
SessionHeader(plan, day, session, state.week, vm)
WeekDayPicker(plan, state.tracker, state.week, state.day, vm)
SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm)
day.rotations.forEach { rotation ->
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
Text(rotation.label, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
rotation.exercises.forEach { exercise ->
ExerciseCard(exercise, session, plan, vm)
}
}
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(rotation.label.uppercase(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
rotation.exercises.forEach { exercise -> ExerciseCard(exercise, session, plan, vm) }
}
}
SequenceCard("Cooldown", "cooldown", day.cooldown, session, vm)
SequenceCard("Stretch", "stretch", day.stretch, session, vm)
Spacer(Modifier.height(24.dp))
Spacer(Modifier.height(20.dp))
}
}
@@ -288,40 +292,26 @@ private fun WeekPicker(plan: PlanUi, tracker: JsonObject, week: Int, vm: Tracker
@Composable
private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: Int, vm: TrackerViewModel) {
val status = session.string("status", "planned")
val statusLabel = when (status) {
"in_progress" -> "Läuft"
"stopped" -> "Gestoppt"
"completed" -> "Abgeschlossen"
else -> "Geplant"
}
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
val statusLabel = when (status) { "in_progress" -> "Läuft"; "stopped" -> "Gestoppt"; "completed" -> "Abgeschlossen"; else -> "Geplant" }
val gradient = Brush.linearGradient(listOf(Color(0xFF4F46E5), Color(0xFF9333EA)))
Column(
Modifier.fillMaxWidth().clip(RoundedCornerShape(26.dp)).background(gradient).padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Column(
Modifier
.background(Brush.linearGradient(listOf(ModernPurple, ModernIndigo)))
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text("Woche $week · Tag ${day.num}", color = Color.White.copy(alpha = .8f), style = MaterialTheme.typography.labelLarge)
Text(day.focus, color = Color.White, style = MaterialTheme.typography.headlineSmall)
Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)} · $statusLabel", color = Color.White.copy(alpha = .78f), style = MaterialTheme.typography.bodySmall)
when (status) {
"in_progress" -> Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Button(onClick = { vm.setSessionStatus("completed") }, modifier = Modifier.weight(1f)) {
Icon(Icons.Default.CheckCircle, null); Spacer(Modifier.width(6.dp)); Text("Abschließen", maxLines = 1)
}
OutlinedButton(onClick = { vm.setSessionStatus("stopped") }, modifier = Modifier.weight(1f)) {
Icon(Icons.Default.Stop, null); Spacer(Modifier.width(6.dp)); Text("Stoppen", maxLines = 1)
}
}
"stopped" -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.PlayArrow, null); Spacer(Modifier.width(6.dp)); Text("Session fortsetzen") }
"completed" -> OutlinedButton(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session wieder öffnen") }
else -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.PlayArrow, null); Spacer(Modifier.width(6.dp)); Text("Session starten") }
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("WOCHE $week · TAG ${day.num}", color = Color.White.copy(alpha = .78f), style = MaterialTheme.typography.labelMedium)
Text(statusLabel, color = Color.White, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
}
Text(day.focus, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = Color.White)
Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)}", color = Color.White.copy(alpha = .72f), style = MaterialTheme.typography.bodySmall)
when (status) {
"in_progress" -> Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { vm.setSessionStatus("completed") }, modifier = Modifier.weight(1f)) { Text("Abschließen") }
OutlinedButton(onClick = { vm.setSessionStatus("stopped") }, modifier = Modifier.weight(1f)) { Text("Stoppen", color = Color.White) }
}
TextButton(onClick = vm::resetSession, modifier = Modifier.fillMaxWidth()) { Text("Session zurücksetzen", color = Color.White.copy(alpha = .82f)) }
"stopped" -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session fortsetzen") }
"completed" -> OutlinedButton(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session wieder öffnen", color = Color.White) }
else -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session starten") }
}
}
}
@@ -329,12 +319,8 @@ private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: I
@Composable
private fun SequenceCard(title: String, prefix: String, items: List<String>, session: JsonObject, vm: TrackerViewModel) {
if (items.isEmpty()) return
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
items.forEachIndexed { index, label ->
val id = "$prefix-$index"
@@ -359,84 +345,56 @@ private fun ExerciseCard(exercise: ExerciseUi, session: JsonObject, plan: PlanUi
var progressionMenu by remember(exercise.id, progression) { mutableStateOf(false) }
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = .72f)),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp),
colors = androidx.compose.material3.CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = androidx.compose.material3.CardDefaults.cardElevation(defaultElevation = 6.dp),
) {
Column(Modifier.fillMaxWidth().padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column(Modifier.weight(1f)) {
Text(exercise.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
if (exercise.cue.isNotBlank()) Text(exercise.cue, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Box(Modifier.size(32.dp).clip(RoundedCornerShape(10.dp)).background(MaterialTheme.colorScheme.primaryContainer))
}
Text("ID ${exercise.id}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Status", style = MaterialTheme.typography.labelMedium)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
listOf("planned" to "Offen", "completed" to "Erledigt", "partial" to "Teilweise", "skipped" to "Übersprungen").forEach { (value, label) ->
FilterChip(
selected = status == value,
onClick = { vm.setItemStatus(exercise.id, value) },
label = { Text(label, maxLines = 2, style = MaterialTheme.typography.labelSmall) },
modifier = Modifier.weight(1f),
)
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column(Modifier.weight(1f)) {
Text(exercise.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
if (exercise.cue.isNotBlank()) Text(exercise.cue, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Surface(
shape = RoundedCornerShape(50),
color = if (status == "completed") Color(0xFF10B981) else MaterialTheme.colorScheme.surfaceVariant,
onClick = { vm.setItemStatus(exercise.id, if (status == "completed") "planned" else "completed") },
) {
Icon(Icons.Default.CheckCircle, null, modifier = Modifier.padding(8.dp), tint = if (status == "completed") Color.White else MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
if (status == "skipped") {
OutlinedTextField(
value = item.string("skip_reason"),
onValueChange = { vm.setItemText(exercise.id, "skip_reason", it) },
modifier = Modifier.fillMaxWidth(),
label = { Text("Grund") },
)
} else {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
listOf("planned" to "Offen", "completed" to "Erledigt", "partial" to "Teilweise", "skipped" to "Skip").forEach { (value, label) ->
FilterChip(selected = status == value, onClick = { vm.setItemStatus(exercise.id, value) }, label = { Text(label, maxLines = 1) }, modifier = Modifier.weight(1f))
}
}
if (status == "skipped") {
OutlinedTextField(value = item.string("skip_reason"), onValueChange = { vm.setItemText(exercise.id, "skip_reason", it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Grund") }, shape = RoundedCornerShape(16.dp))
} else {
OutlinedTextField(
value = progression,
onValueChange = { vm.setItemProgression(exercise.id, it) },
modifier = Modifier.fillMaxWidth(),
label = { Text("Progressionsstufe") },
supportingText = { Text("Stufe überschreibt bei Bedarf das Ergebnisschema") },
shape = RoundedCornerShape(16.dp),
)
if (progressionModel?.steps?.isNotEmpty() == true) {
Box {
OutlinedButton(onClick = { progressionMenu = true }, modifier = Modifier.fillMaxWidth()) {
Text("Stufe aus Plan auswählen")
}
OutlinedButton(onClick = { progressionMenu = true }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp)) { Text("Stufe auswählen") }
DropdownMenu(expanded = progressionMenu, onDismissRequest = { progressionMenu = false }) {
progressionModel.steps.forEach { step ->
DropdownMenuItem(
text = { Text(step.name) },
onClick = { progressionMenu = false; vm.setItemProgression(exercise.id, step.name, step.id) },
)
}
progressionModel.steps.forEach { step -> DropdownMenuItem(text = { Text(step.name) }, onClick = { progressionMenu = false; vm.setItemProgression(exercise.id, step.name, step.id) }) }
}
}
}
ManualResultFormatEditor(inherited = schema, manualOverride = item["result_format"] as? JsonObject, onChange = { vm.setItemResultFormat(exercise.id, it) })
ResultEditor(schema, data) { vm.setItemResultData(exercise.id, it) }
OutlinedTextField(value = item.string("note"), onValueChange = { vm.setItemText(exercise.id, "note", it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Sessionnotiz") }, minLines = 2, shape = RoundedCornerShape(16.dp))
}
ManualResultFormatEditor(
inherited = schema,
manualOverride = item["result_format"] as? JsonObject,
onChange = { vm.setItemResultFormat(exercise.id, it) },
)
ResultEditor(schema, data) { vm.setItemResultData(exercise.id, it) }
OutlinedTextField(
value = item.string("note"),
onValueChange = { vm.setItemText(exercise.id, "note", it) },
modifier = Modifier.fillMaxWidth(),
label = { Text("Sessionnotiz") },
minLines = 2,
)
}
}
}
}