diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt index ed8b8e1..b6714b9 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt @@ -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") } }, ) } diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt index 0c24d82..8661e35 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt @@ -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) } diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt index 8152c91..c6aeb9e 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt @@ -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, 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, 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, - ) } } - } }