UI
This commit is contained in:
@@ -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 = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
) {
|
||||
NavigationBar(containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface) {
|
||||
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)
|
||||
}
|
||||
|
||||
+102
-144
@@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Box {
|
||||
IconButton(onClick = { planMenu = true }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "Plan wechseln")
|
||||
}
|
||||
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) },
|
||||
)
|
||||
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,
|
||||
}
|
||||
|
||||
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) ->
|
||||
Tab(
|
||||
selected = state.tab == id,
|
||||
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 = { Text(label, maxLines = 2, style = MaterialTheme.typography.labelMedium) },
|
||||
) {
|
||||
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
|
||||
.background(Brush.linearGradient(listOf(ModernPurple, ModernIndigo)))
|
||||
.padding(20.dp),
|
||||
Modifier.fillMaxWidth().clip(RoundedCornerShape(26.dp)).background(gradient).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)
|
||||
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(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)
|
||||
"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) }
|
||||
}
|
||||
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
|
||||
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,81 +345,53 @@ 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)) {
|
||||
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.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)
|
||||
}
|
||||
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),
|
||||
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") },
|
||||
) {
|
||||
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),
|
||||
)
|
||||
Icon(Icons.Default.CheckCircle, null, modifier = Modifier.padding(8.dp), tint = if (status == "completed") Color.White else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
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") },
|
||||
)
|
||||
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 {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
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) },
|
||||
)
|
||||
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,
|
||||
)
|
||||
OutlinedTextField(value = item.string("note"), onValueChange = { vm.setItemText(exercise.id, "note", it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Sessionnotiz") }, minLines = 2, shape = RoundedCornerShape(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user