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 package de.drazz.boehmitools.training.ui
import androidx.compose.foundation.layout.Box 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.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.NavigationBarItemDefaults
@@ -19,6 +21,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@@ -48,16 +51,19 @@ fun AppRoot(
val backStack by navController.currentBackStackEntryAsState() val backStack by navController.currentBackStackEntryAsState()
Scaffold( Scaffold(
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
bottomBar = { bottomBar = {
Box(Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp)) { Box(Modifier.padding(horizontal = 18.dp, vertical = 12.dp)) {
Surface( Surface(
modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(28.dp),
shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surface,
tonalElevation = 10.dp, shadowElevation = 14.dp,
shadowElevation = 12.dp, tonalElevation = 4.dp,
) { ) {
NavigationBar(containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface) { NavigationBar(
containerColor = Color.Transparent,
tonalElevation = 0.dp,
) {
destinations.forEach { destination -> destinations.forEach { destination ->
val selected = backStack?.destination?.hierarchy?.any { it.route == destination.route } == true val selected = backStack?.destination?.hierarchy?.any { it.route == destination.route } == true
NavigationBarItem( NavigationBarItem(
@@ -72,7 +78,9 @@ fun AppRoot(
icon = destination.icon, icon = destination.icon,
label = { Text(destination.label) }, label = { Text(destination.label) },
colors = NavigationBarItemDefaults.colors( 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 -> ) { padding ->
NavHost(navController = navController, startDestination = "tracker") { NavHost(navController = navController, startDestination = "tracker", modifier = Modifier.fillMaxSize()) {
composable("tracker") { TrackerScreen(contentPadding = padding) } composable("tracker") { TrackerScreen(contentPadding = padding) }
composable("settings") { composable("settings") {
SettingsScreen( SettingsScreen(
@@ -102,7 +110,7 @@ private fun WhatsNewDialog(onDismiss: () -> Unit) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Neu in RipMe ${BuildConfig.VERSION_NAME}") }, 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") } }, 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.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color 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( private val Light = lightColorScheme(
primary = ModernPurple, primary = Color(0xFF7C3AED),
onPrimary = Color.White, onPrimary = Color.White,
primaryContainer = Color(0xFFEDE9FE), primaryContainer = Color(0xFFEDE9FE),
onPrimaryContainer = Color(0xFF32136B), onPrimaryContainer = Color(0xFF2E1065),
secondary = ModernIndigo, secondary = Color(0xFF4F46E5),
onSecondary = Color.White, onSecondary = Color.White,
secondaryContainer = Color(0xFFE0E7FF), secondaryContainer = Color(0xFFE0E7FF),
onSecondaryContainer = Color(0xFF1E1B4B), tertiary = Color(0xFF06B6D4),
tertiary = ModernGreen, background = Color(0xFFF4F5FB),
background = Color(0xFFF6F5FA), onBackground = Color(0xFF171725),
onBackground = Color(0xFF17151D),
surface = Color(0xFFFFFFFF), surface = Color(0xFFFFFFFF),
onSurface = Color(0xFF17151D), onSurface = Color(0xFF171725),
surfaceVariant = Color(0xFFF0EEF5), surfaceVariant = Color(0xFFF0F1F7),
onSurfaceVariant = Color(0xFF66616F), onSurfaceVariant = Color(0xFF69697A),
outline = Color(0xFFDDD8E7), outline = Color(0xFFD9DAE5),
outlineVariant = Color(0xFFEAE6F0),
) )
private val Dark = darkColorScheme( private val Dark = darkColorScheme(
primary = Color(0xFFA78BFA), primary = Color(0xFFA78BFA),
onPrimary = Color(0xFF221047), onPrimary = Color(0xFF24005B),
primaryContainer = Color(0xFF4C1D95), primaryContainer = Color(0xFF4C1D95),
onPrimaryContainer = Color(0xFFF3E8FF), onPrimaryContainer = Color(0xFFEDE9FE),
secondary = Color(0xFF818CF8), secondary = Color(0xFF818CF8),
onSecondary = Color(0xFF11133F), onSecondary = Color(0xFF10144A),
secondaryContainer = Color(0xFF312E81), secondaryContainer = Color(0xFF312E81),
onSecondaryContainer = Color(0xFFE0E7FF), tertiary = Color(0xFF22D3EE),
tertiary = Color(0xFF34D399), background = Color(0xFF0B0D17),
background = Color(0xFF0D111A), onBackground = Color(0xFFF5F5FA),
onBackground = Color(0xFFF5F3FA), surface = Color(0xFF151824),
surface = Color(0xFF151A25), onSurface = Color(0xFFF5F5FA),
onSurface = Color(0xFFF5F3FA), surfaceVariant = Color(0xFF202432),
surfaceVariant = Color(0xFF202634), onSurfaceVariant = Color(0xFFB8BBC8),
onSurfaceVariant = Color(0xFFB7B2C3), outline = Color(0xFF343849),
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),
) )
@Composable @Composable
@@ -69,9 +50,5 @@ fun TrainingTheme(theme: String, content: @Composable () -> Unit) {
"light" -> false "light" -> false
else -> isSystemInDarkTheme() else -> isSystemInDarkTheme()
} }
MaterialTheme( MaterialTheme(colorScheme = if (dark) Dark else Light, content = content)
colorScheme = if (dark) Dark else Light,
typography = ModernTypography,
content = content,
)
} }
@@ -11,15 +11,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll 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.Icons
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.MoreVert 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@@ -45,24 +40,21 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.ui.text.font.FontWeight
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp 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.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.drazz.boehmitools.training.data.local.PendingMutation import de.drazz.boehmitools.training.data.local.PendingMutation
@@ -100,50 +92,71 @@ fun TrackerScreen(
val pending by viewModel.pending.collectAsStateWithLifecycle() val pending by viewModel.pending.collectAsStateWithLifecycle()
val currentPending = pending.filter { it.planId == state.selectedPlanId } val currentPending = pending.filter { it.planId == state.selectedPlanId }
var planMenu by remember { mutableStateOf(false) } var planMenu by remember { mutableStateOf(false) }
val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ")
Scaffold( Column(
modifier = Modifier.padding(contentPadding), Modifier
containerColor = MaterialTheme.colorScheme.background, .fillMaxSize()
topBar = { .padding(contentPadding)
TopAppBar( .background(MaterialTheme.colorScheme.background),
title = { ) {
Column { Row(
Text(state.plan?.title ?: "RipMe", maxLines = 2) Modifier.fillMaxWidth().padding(start = 20.dp, end = 12.dp, top = 16.dp, bottom = 10.dp),
if (state.saveState.isNotBlank()) Text(state.saveState, style = MaterialTheme.typography.labelSmall) 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") Row(
} Modifier
DropdownMenu(expanded = planMenu, onDismissRequest = { planMenu = false }) { .padding(horizontal = 16.dp, vertical = 4.dp)
state.plans.forEach { plan -> .fillMaxWidth()
DropdownMenuItem( .clip(RoundedCornerShape(18.dp))
text = { Text(plan.title) }, .background(MaterialTheme.colorScheme.surfaceVariant)
onClick = { planMenu = false; viewModel.selectPlan(plan.id) }, .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),
) { inner -> color = if (selected) MaterialTheme.colorScheme.surface else Color.Transparent,
Column(Modifier.fillMaxSize().padding(inner)) { shadowElevation = if (selected) 3.dp else 0.dp,
val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ") onClick = { viewModel.setTab(id) },
PrimaryTabRow( ) {
selectedTabIndex = tabs.indexOfFirst { it.first == state.tab }.coerceAtLeast(0), Text(
containerColor = MaterialTheme.colorScheme.background, label,
) { modifier = Modifier.padding(vertical = 10.dp),
tabs.forEach { (id, label) -> textAlign = androidx.compose.ui.text.style.TextAlign.Center,
Tab( style = MaterialTheme.typography.labelMedium,
selected = state.tab == id, fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
onClick = { viewModel.setTab(id) }, color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
text = { Text(label, maxLines = 2, style = MaterialTheme.typography.labelMedium) },
) )
} }
} }
}
Box(Modifier.fillMaxSize()) {
when { when {
state.loading && state.plan == null -> Box(Modifier.fillMaxSize().padding(32.dp)) { CircularProgressIndicator() } 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) } state.error.isNotBlank() && state.plan == null -> ErrorPane(state.error) { viewModel.loadPlans(force = true) }
@@ -162,7 +175,7 @@ fun TrackerScreen(
AlertDialog( AlertDialog(
onDismissRequest = viewModel::dismissConflict, onDismissRequest = viewModel::dismissConflict,
title = { Text("Revisionskonflikt") }, 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") } }, confirmButton = { TextButton(onClick = viewModel::reloadAfterConflict) { Text("Serverstand neu laden") } },
dismissButton = { TextButton(onClick = viewModel::dismissConflict) { Text("Lokale Ansicht behalten") } }, 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 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) val session = trackerSession(state.tracker, de.drazz.boehmitools.training.domain.sessionKey(state.week, state.day), plan)
Column( 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), verticalArrangement = Arrangement.spacedBy(14.dp),
) { ) {
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error)
if (pending.isNotEmpty()) PendingQueue(pending, vm) if (pending.isNotEmpty()) PendingQueue(pending, vm)
WeekDayPicker(plan, state.tracker, state.week, state.day, vm)
SessionHeader(plan, day, session, state.week, 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) SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm)
day.rotations.forEach { rotation -> day.rotations.forEach { rotation ->
Card( Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
modifier = Modifier.fillMaxWidth(), Text(rotation.label.uppercase(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
shape = RoundedCornerShape(22.dp), rotation.exercises.forEach { exercise -> ExerciseCard(exercise, session, plan, vm) }
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)
}
}
} }
} }
SequenceCard("Cooldown", "cooldown", day.cooldown, session, vm) SequenceCard("Cooldown", "cooldown", day.cooldown, session, vm)
SequenceCard("Stretch", "stretch", day.stretch, 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 @Composable
private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: Int, vm: TrackerViewModel) { private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: Int, vm: TrackerViewModel) {
val status = session.string("status", "planned") val status = session.string("status", "planned")
val statusLabel = when (status) { val statusLabel = when (status) { "in_progress" -> "Läuft"; "stopped" -> "Gestoppt"; "completed" -> "Abgeschlossen"; else -> "Geplant" }
"in_progress" -> "Läuft" val gradient = Brush.linearGradient(listOf(Color(0xFF4F46E5), Color(0xFF9333EA)))
"stopped" -> "Gestoppt" Column(
"completed" -> "Abgeschlossen" Modifier.fillMaxWidth().clip(RoundedCornerShape(26.dp)).background(gradient).padding(20.dp),
else -> "Geplant" verticalArrangement = Arrangement.spacedBy(12.dp),
}
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
) { ) {
Column( Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Modifier Text("WOCHE $week · TAG ${day.num}", color = Color.White.copy(alpha = .78f), style = MaterialTheme.typography.labelMedium)
.background(Brush.linearGradient(listOf(ModernPurple, ModernIndigo))) Text(statusLabel, color = Color.White, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
.padding(20.dp), }
verticalArrangement = Arrangement.spacedBy(12.dp), 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)
Text("Woche $week · Tag ${day.num}", color = Color.White.copy(alpha = .8f), style = MaterialTheme.typography.labelLarge) when (status) {
Text(day.focus, color = Color.White, style = MaterialTheme.typography.headlineSmall) "in_progress" -> Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)} · $statusLabel", color = Color.White.copy(alpha = .78f), style = MaterialTheme.typography.bodySmall) Button(onClick = { vm.setSessionStatus("completed") }, modifier = Modifier.weight(1f)) { Text("Abschließen") }
when (status) { OutlinedButton(onClick = { vm.setSessionStatus("stopped") }, modifier = Modifier.weight(1f)) { Text("Stoppen", color = Color.White) }
"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") }
} }
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 @Composable
private fun SequenceCard(title: String, prefix: String, items: List<String>, session: JsonObject, vm: TrackerViewModel) { private fun SequenceCard(title: String, prefix: String, items: List<String>, session: JsonObject, vm: TrackerViewModel) {
if (items.isEmpty()) return if (items.isEmpty()) return
Card( Card(Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(), Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
items.forEachIndexed { index, label -> items.forEachIndexed { index, label ->
val id = "$prefix-$index" 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) } var progressionMenu by remember(exercise.id, progression) { mutableStateOf(false) }
Card( Card(
modifier = Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp), shape = RoundedCornerShape(22.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = .72f)), colors = androidx.compose.material3.CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), elevation = androidx.compose.material3.CardDefaults.cardElevation(defaultElevation = 6.dp),
) { ) {
Column(Modifier.fillMaxWidth().padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text(exercise.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) 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) 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)) Surface(
} shape = RoundedCornerShape(50),
Text("ID ${exercise.id}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) color = if (status == "completed") Color(0xFF10B981) else MaterialTheme.colorScheme.surfaceVariant,
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { onClick = { vm.setItemStatus(exercise.id, if (status == "completed") "planned" else "completed") },
Text("Status", style = MaterialTheme.typography.labelMedium) ) {
Row( Icon(Icons.Default.CheckCircle, null, modifier = Modifier.padding(8.dp), tint = if (status == "completed") Color.White else MaterialTheme.colorScheme.onSurfaceVariant)
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),
)
} }
} }
}
if (status == "skipped") { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
OutlinedTextField( listOf("planned" to "Offen", "completed" to "Erledigt", "partial" to "Teilweise", "skipped" to "Skip").forEach { (value, label) ->
value = item.string("skip_reason"), FilterChip(selected = status == value, onClick = { vm.setItemStatus(exercise.id, value) }, label = { Text(label, maxLines = 1) }, modifier = Modifier.weight(1f))
onValueChange = { vm.setItemText(exercise.id, "skip_reason", it) }, }
modifier = Modifier.fillMaxWidth(), }
label = { Text("Grund") },
) if (status == "skipped") {
} else { OutlinedTextField(value = item.string("skip_reason"), onValueChange = { vm.setItemText(exercise.id, "skip_reason", it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Grund") }, shape = RoundedCornerShape(16.dp))
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { } else {
OutlinedTextField( OutlinedTextField(
value = progression, value = progression,
onValueChange = { vm.setItemProgression(exercise.id, it) }, onValueChange = { vm.setItemProgression(exercise.id, it) },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text("Progressionsstufe") }, label = { Text("Progressionsstufe") },
supportingText = { Text("Stufe überschreibt bei Bedarf das Ergebnisschema") }, shape = RoundedCornerShape(16.dp),
) )
if (progressionModel?.steps?.isNotEmpty() == true) { if (progressionModel?.steps?.isNotEmpty() == true) {
Box { Box {
OutlinedButton(onClick = { progressionMenu = true }, modifier = Modifier.fillMaxWidth()) { OutlinedButton(onClick = { progressionMenu = true }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp)) { Text("Stufe auswählen") }
Text("Stufe aus Plan auswählen")
}
DropdownMenu(expanded = progressionMenu, onDismissRequest = { progressionMenu = false }) { DropdownMenu(expanded = progressionMenu, onDismissRequest = { progressionMenu = false }) {
progressionModel.steps.forEach { step -> progressionModel.steps.forEach { step -> DropdownMenuItem(text = { Text(step.name) }, onClick = { progressionMenu = false; vm.setItemProgression(exercise.id, step.name, step.id) }) }
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,
)
} }
} }
}
} }