Android UI

This commit is contained in:
2026-07-27 08:03:29 +02:00
parent 1744e444b7
commit 4490dd3126
6 changed files with 293 additions and 138 deletions
@@ -1,18 +1,25 @@
package de.drazz.boehmitools.training.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
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.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@@ -35,14 +42,24 @@ fun AppRoot(
) {
val navController = rememberNavController()
val destinations = listOf(
AppDestination("tracker", "Tracker") { Icon(Icons.Default.FitnessCenter, null) },
AppDestination("settings", "Einstellungen") { Icon(Icons.Default.Settings, null) },
AppDestination("tracker", "Training") { Icon(Icons.Default.FitnessCenter, null) },
AppDestination("settings", "Mehr") { Icon(Icons.Default.Settings, null) },
)
val backStack by navController.currentBackStackEntryAsState()
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
bottomBar = {
NavigationBar {
Surface(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
shape = RoundedCornerShape(24.dp),
tonalElevation = 8.dp,
shadowElevation = 10.dp,
) {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surface,
windowInsets = NavigationBarDefaults.windowInsets,
) {
destinations.forEach { destination ->
val selected = backStack?.destination?.hierarchy?.any { it.route == destination.route } == true
NavigationBarItem(
@@ -56,12 +73,16 @@ fun AppRoot(
},
icon = destination.icon,
label = { Text(destination.label) },
colors = NavigationBarItemDefaults.colors(
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
}
}
},
) { padding ->
NavHost(navController = navController, startDestination = "tracker", modifier = Modifier) {
NavHost(navController = navController, startDestination = "tracker") {
composable("tracker") { TrackerScreen(contentPadding = padding) }
composable("settings") {
SettingsScreen(
@@ -74,9 +95,7 @@ fun AppRoot(
}
}
if (showWhatsNew) {
WhatsNewDialog(onDismiss = onDismissWhatsNew)
}
if (showWhatsNew) WhatsNewDialog(onDismiss = onDismissWhatsNew)
}
@Composable
@@ -84,17 +103,7 @@ private fun WhatsNewDialog(onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Neu in RipMe ${BuildConfig.VERSION_NAME}") },
text = {
Text(
"• RipMe konzentriert sich vollständig auf den Trainingstracker.\n" +
"• Analyseergebnisse werden jetzt vollständig in der App angezeigt.\n" +
"• Navigation und Statusfelder bleiben auch auf schmalen Displays fest im Bildschirm.\n" +
"• Wochen und Trainingstage zeigen ihren aktuellen Abschlussstatus.\n" +
"• Offline erfasste Sessionänderungen werden weiterhin sicher synchronisiert.",
)
},
confirmButton = {
TextButton(onClick = onDismiss) { Text("Verstanden") }
},
text = { Text("Moderner Card-Look, verbesserte Hierarchie und kompaktere Trainingseingaben. Alle bestehenden Daten- und Synchronisationsfunktionen bleiben erhalten.") },
confirmButton = { TextButton(onClick = onDismiss) { Text("Verstanden") } },
)
}
@@ -1,6 +1,8 @@
package de.drazz.boehmitools.training.ui.setup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -11,13 +13,17 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudDone
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -27,12 +33,17 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.drazz.boehmitools.training.BuildConfig
import de.drazz.boehmitools.training.data.settings.SettingsRepository
import de.drazz.boehmitools.training.ui.theme.BrandIndigo
import de.drazz.boehmitools.training.ui.theme.BrandPurple
import de.drazz.boehmitools.training.ui.theme.BrandViolet
@Composable
fun SettingsScreen(
@@ -48,65 +59,63 @@ fun SettingsScreen(
.fillMaxSize()
.padding(contentPadding)
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
.padding(horizontal = 14.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
Text("RipMe Einstellungen", style = MaterialTheme.typography.headlineMedium)
Box(
Modifier
.fillMaxWidth()
.background(Brush.linearGradient(listOf(BrandIndigo, BrandPurple, BrandViolet)), RoundedCornerShape(26.dp))
.padding(20.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Einstellungen", color = Color.White, style = MaterialTheme.typography.headlineMedium)
Text("Verbindung, Darstellung und App-Informationen", color = Color.White.copy(alpha = 0.82f))
}
}
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Icon(Icons.Default.Lock, null, tint = MaterialTheme.colorScheme.primary)
Text("Server & Anmeldung", style = MaterialTheme.typography.titleLarge)
}
Text(
"Basic-Auth-Zugangsdaten werden mit einem Android-Keystore-Schlüssel verschlüsselt. Release-Builds akzeptieren ausschließlich HTTPS.",
"Zugangsdaten werden lokal mit Android-Keystore-Unterstützung geschützt. Release-Builds akzeptieren ausschließlich HTTPS.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
)
OutlinedTextField(state.serverUrl, viewModel::updateServer, Modifier.fillMaxWidth(), label = { Text("Server-URL") }, singleLine = true)
OutlinedTextField(state.username, viewModel::updateUsername, Modifier.fillMaxWidth(), label = { Text("Benutzername") }, singleLine = true)
OutlinedTextField(
value = state.serverUrl,
onValueChange = viewModel::updateServer,
modifier = Modifier.fillMaxWidth(),
label = { Text("Server-URL") },
singleLine = true,
)
OutlinedTextField(
value = state.username,
onValueChange = viewModel::updateUsername,
modifier = Modifier.fillMaxWidth(),
label = { Text("Benutzername") },
singleLine = true,
)
OutlinedTextField(
value = state.password,
onValueChange = viewModel::updatePassword,
modifier = Modifier.fillMaxWidth(),
state.password,
viewModel::updatePassword,
Modifier.fillMaxWidth(),
label = { Text("Passwort") },
visualTransformation = PasswordVisualTransformation(),
singleLine = true,
)
Button(
onClick = viewModel::saveAndTest,
enabled = !state.loading,
modifier = Modifier.fillMaxWidth(),
) {
if (state.loading) {
CircularProgressIndicator(modifier = Modifier.height(18.dp), strokeWidth = 2.dp)
} else {
Icon(Icons.Default.CloudDone, null)
}
Button(onClick = viewModel::saveAndTest, enabled = !state.loading, modifier = Modifier.fillMaxWidth()) {
if (state.loading) CircularProgressIndicator(modifier = Modifier.height(18.dp), strokeWidth = 2.dp)
else Icon(Icons.Default.CloudDone, null)
Spacer(Modifier.width(8.dp))
Text("Speichern & Verbindung testen")
}
if (state.message.isNotBlank()) Text(state.message, color = MaterialTheme.colorScheme.primary)
if (state.message.isNotBlank()) Text(state.message, color = MaterialTheme.colorScheme.tertiary)
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error)
}
}
Card(Modifier.fillMaxWidth()) {
ElevatedCard(Modifier.fillMaxWidth(), elevation = CardDefaults.elevatedCardElevation(3.dp)) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Icon(Icons.Default.Palette, null, tint = MaterialTheme.colorScheme.primary)
Text("Darstellung", style = MaterialTheme.typography.titleLarge)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
listOf("system" to "System", "light" to "Hell", "dark" to "Dunkel").forEach { (value, label) ->
FilterChip(
selected = current.theme == value,
@@ -119,14 +128,11 @@ fun SettingsScreen(
}
}
Card(Modifier.fillMaxWidth()) {
ElevatedCard(Modifier.fillMaxWidth(), elevation = CardDefaults.elevatedCardElevation(3.dp)) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Über RipMe", style = MaterialTheme.typography.titleLarge)
Text("Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
Text(
"Native Android-App für deinen Trainingstracker.",
style = MaterialTheme.typography.bodyMedium,
)
Text("Native Android-App für deinen Trainingstracker.", color = MaterialTheme.colorScheme.onSurfaceVariant)
OutlinedButton(onClick = onShowWhatsNew, modifier = Modifier.fillMaxWidth()) {
Icon(Icons.Default.Info, null)
Spacer(Modifier.width(8.dp))
@@ -1,39 +1,77 @@
package de.drazz.boehmitools.training.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
val BrandPurple = Color(0xFF7C4DFF)
val BrandViolet = Color(0xFF9C4DFF)
val BrandIndigo = Color(0xFF5B5FEF)
val SuccessGreen = Color(0xFF22C58B)
private val Light = lightColorScheme(
primary = Color(0xFF566600),
primary = Color(0xFF6D3FE8),
onPrimary = Color.White,
primaryContainer = Color(0xFFD9F26A),
onPrimaryContainer = Color(0xFF172000),
secondary = Color(0xFF53643C),
tertiary = Color(0xFF00696C),
background = Color(0xFFF9FAF2),
surface = Color(0xFFF9FAF2),
surfaceVariant = Color(0xFFE5E8D8),
primaryContainer = Color(0xFFE9DDFF),
onPrimaryContainer = Color(0xFF22005D),
secondary = Color(0xFF64558A),
onSecondary = Color.White,
secondaryContainer = Color(0xFFE9DEFF),
onSecondaryContainer = Color(0xFF201047),
tertiary = Color(0xFF006C63),
tertiaryContainer = Color(0xFF74F8E8),
background = Color(0xFFF8F7FC),
onBackground = Color(0xFF1C1B20),
surface = Color(0xFFFFFBFF),
onSurface = Color(0xFF1C1B20),
surfaceVariant = Color(0xFFE8E5EE),
outline = Color(0xFF7A757F),
outlineVariant = Color(0xFFCBC5D0),
)
private val Dark = darkColorScheme(
primary = Color(0xFFC0D852),
onPrimary = Color(0xFF2B3400),
primaryContainer = Color(0xFF414D00),
onPrimaryContainer = Color(0xFFD9F26A),
secondary = Color(0xFFBACCA2),
tertiary = Color(0xFF80D4D6),
background = Color(0xFF11140E),
surface = Color(0xFF11140E),
surfaceVariant = Color(0xFF41443A),
primary = Color(0xFFCBB5FF),
onPrimary = Color(0xFF3B008F),
primaryContainer = Color(0xFF5427B6),
onPrimaryContainer = Color(0xFFEADDFF),
secondary = Color(0xFFCDC1E9),
onSecondary = Color(0xFF352A4F),
secondaryContainer = Color(0xFF4C4167),
onSecondaryContainer = Color(0xFFE9DEFF),
tertiary = Color(0xFF54DBC9),
tertiaryContainer = Color(0xFF005049),
background = Color(0xFF0F1118),
onBackground = Color(0xFFE8E1EA),
surface = Color(0xFF151821),
onSurface = Color(0xFFE8E1EA),
surfaceVariant = Color(0xFF2A2D38),
outline = Color(0xFF958F9A),
outlineVariant = Color(0xFF46434D),
)
private val AppShapes = Shapes(
extraSmall = RoundedCornerShape(10.dp),
small = RoundedCornerShape(14.dp),
medium = RoundedCornerShape(20.dp),
large = RoundedCornerShape(26.dp),
extraLarge = RoundedCornerShape(32.dp),
)
private val AppTypography = Typography(
headlineMedium = Typography().headlineMedium.copy(fontWeight = FontWeight.Bold, letterSpacing = (-0.4).sp),
headlineSmall = Typography().headlineSmall.copy(fontWeight = FontWeight.Bold, letterSpacing = (-0.3).sp),
titleLarge = Typography().titleLarge.copy(fontWeight = FontWeight.SemiBold),
titleMedium = Typography().titleMedium.copy(fontWeight = FontWeight.SemiBold),
labelLarge = Typography().labelLarge.copy(fontWeight = FontWeight.SemiBold),
)
@Composable
@@ -43,5 +81,10 @@ fun TrainingTheme(theme: String, content: @Composable () -> Unit) {
"light" -> false
else -> isSystemInDarkTheme()
}
MaterialTheme(colorScheme = if (dark) Dark else Light, content = content)
MaterialTheme(
colorScheme = if (dark) Dark else Light,
typography = AppTypography,
shapes = AppShapes,
content = content,
)
}
@@ -1,5 +1,6 @@
package de.drazz.boehmitools.training.ui.tracker
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.material.icons.Icons
@@ -24,6 +26,8 @@ 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.ElevatedCard
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
@@ -38,6 +42,7 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.TabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
@@ -49,10 +54,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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.BrandIndigo
import de.drazz.boehmitools.training.ui.theme.BrandPurple
import de.drazz.boehmitools.training.ui.theme.BrandViolet
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.drazz.boehmitools.training.data.local.PendingMutation
@@ -97,7 +107,7 @@ fun TrackerScreen(
TopAppBar(
title = {
Column {
Text(state.plan?.title ?: "RipMe", maxLines = 2)
Text(state.plan?.title ?: "RipMe", maxLines = 1, style = MaterialTheme.typography.titleLarge)
if (state.saveState.isNotBlank()) Text(state.saveState, style = MaterialTheme.typography.labelSmall)
}
},
@@ -121,7 +131,12 @@ fun TrackerScreen(
) { inner ->
Column(Modifier.fillMaxSize().padding(inner)) {
val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ")
TabRow(selectedTabIndex = tabs.indexOfFirst { it.first == state.tab }.coerceAtLeast(0)) {
TabRow(
selectedTabIndex = tabs.indexOfFirst { it.first == state.tab }.coerceAtLeast(0),
containerColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.primary,
divider = {},
) {
tabs.forEach { (id, label) ->
Tab(
selected = state.tab == id,
@@ -169,7 +184,7 @@ 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(12.dp),
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error)
@@ -179,12 +194,22 @@ private fun SessionPane(state: TrackerUiState, pending: List<PendingMutation>, v
SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm)
day.rotations.forEach { rotation ->
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(rotation.label, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
rotation.exercises.forEach { exercise ->
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(18.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.42f),
) {
Box(Modifier.padding(14.dp)) {
ExerciseCard(exercise, session, plan, vm)
HorizontalDivider()
}
}
}
}
}
@@ -197,7 +222,10 @@ private fun SessionPane(state: TrackerUiState, pending: List<PendingMutation>, v
@Composable
private fun PendingQueue(pending: List<PendingMutation>, vm: TrackerViewModel) {
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.CloudOff, null)
@@ -276,11 +304,19 @@ private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: I
"completed" -> "Abgeschlossen"
else -> "Geplant"
}
Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Woche $week · Tag ${day.num}", style = MaterialTheme.typography.labelLarge)
Text(day.focus, style = MaterialTheme.typography.headlineSmall)
Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)} · Status: $statusLabel", style = MaterialTheme.typography.bodySmall)
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 6.dp),
) {
Column(
Modifier
.background(Brush.linearGradient(listOf(BrandIndigo, BrandPurple, BrandViolet)))
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text("Woche $week · Tag ${day.num}", color = Color.White.copy(alpha = 0.86f), style = MaterialTheme.typography.labelLarge)
Text(day.focus, color = Color.White, style = MaterialTheme.typography.headlineSmall)
Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)} · Status: $statusLabel", color = Color.White.copy(alpha = 0.78f), style = MaterialTheme.typography.bodySmall)
when (status) {
"in_progress" -> Row(
modifier = Modifier.fillMaxWidth(),
@@ -289,6 +325,10 @@ private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: I
Button(
onClick = { vm.setSessionStatus("completed") },
modifier = Modifier.weight(1f),
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = BrandPurple,
),
) {
Icon(Icons.Default.CheckCircle, null)
Spacer(Modifier.width(6.dp))
@@ -334,7 +374,10 @@ 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.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
items.forEachIndexed { index, label ->
@@ -362,14 +405,14 @@ private fun ExerciseCard(exercise: ExerciseUi, session: JsonObject, plan: PlanUi
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(exercise.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
if (exercise.cue.isNotBlank()) Text(exercise.cue, style = MaterialTheme.typography.bodySmall)
Text("ID ${exercise.id}", style = MaterialTheme.typography.labelSmall)
Text("Übungs-ID · ${exercise.id}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelSmall)
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) ->
listOf("planned" to "Offen", "completed" to "Fertig", "partial" to "Teilw.", "skipped" to "Skip").forEach { (value, label) ->
FilterChip(
selected = status == value,
onClick = { vm.setItemStatus(exercise.id, value) },
@@ -459,7 +502,10 @@ private fun ManualResultFormatEditor(
return JsonObject(values)
}
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column(Modifier.weight(1f)) {
@@ -505,7 +551,10 @@ private fun ChoiceChips(label: String, choices: List<String>, selected: String,
@Composable
private fun ResultEditor(schema: ResultSchemaUi, data: JsonObject, onChange: (JsonObject) -> Unit) {
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Ergebnis · ${schema.source}", style = MaterialTheme.typography.labelLarge)
Text("${schema.mode}, ${schema.laterality}, Gewicht ${schema.weightMode}", style = MaterialTheme.typography.bodySmall)
@@ -602,12 +651,15 @@ private fun ProgressPane(state: TrackerUiState, pending: List<PendingMutation>,
}
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp),
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text("Fortschritt", style = MaterialTheme.typography.headlineSmall)
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Lokale Übersicht", style = MaterialTheme.typography.titleMedium)
Text("$completed abgeschlossene Sessions · $stopped gestoppte Sessions · ${sessions.size} angelegte Sessions")
@@ -617,7 +669,10 @@ private fun ProgressPane(state: TrackerUiState, pending: List<PendingMutation>,
if (pending.isNotEmpty()) PendingQueue(pending, vm)
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Auswertung auswählen", style = MaterialTheme.typography.titleMedium)
Row(
@@ -645,7 +700,10 @@ private fun ProgressPane(state: TrackerUiState, pending: List<PendingMutation>,
}
if (state.analysisScope == "week") {
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Wochenstatus", style = MaterialTheme.typography.titleMedium)
val closed = state.tracker.obj("week_statuses").obj(state.week.toString()).string("status") == "closed"
@@ -663,7 +721,10 @@ private fun ProgressPane(state: TrackerUiState, pending: List<PendingMutation>,
}
}
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Manuelle Analyse", style = MaterialTheme.typography.titleMedium)
Button(
@@ -697,7 +758,10 @@ private fun ProgressPane(state: TrackerUiState, pending: List<PendingMutation>,
@Composable
private fun AnalysisCard(title: String, record: JsonObject?) {
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium)
if (record == null) {
@@ -727,7 +791,10 @@ private fun AnalysisCard(title: String, record: JsonObject?) {
val dataQuality = result.string("data_quality", result.obj("overview").string("data_quality"))
if (dataQuality.isNotBlank()) {
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Datenbasis", fontWeight = FontWeight.Bold)
Text(dataQuality)
@@ -739,7 +806,10 @@ private fun AnalysisCard(title: String, record: JsonObject?) {
if (metrics.isNotEmpty()) {
Text("Kennzahlen", style = MaterialTheme.typography.titleMedium)
metrics.forEach { metric ->
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(metric.string("label"), style = MaterialTheme.typography.labelMedium)
Text(metric.string("value"), style = MaterialTheme.typography.titleMedium)
@@ -757,7 +827,10 @@ private fun AnalysisCard(title: String, record: JsonObject?) {
if (exercises.isNotEmpty()) {
Text("Übungsstand", style = MaterialTheme.typography.titleMedium)
exercises.forEach { update ->
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(update.string("name", "Übung"), fontWeight = FontWeight.Bold)
val trend = when (update.string("trend")) {
@@ -785,7 +858,10 @@ private fun AnalysisCard(title: String, record: JsonObject?) {
style = MaterialTheme.typography.bodySmall,
)
adjustments.forEach { item ->
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(item.string("target", "Vorschlag"), fontWeight = FontWeight.Bold)
if (item.string("suggested_change").isNotBlank()) Text(item.string("suggested_change"))
@@ -799,7 +875,10 @@ private fun AnalysisCard(title: String, record: JsonObject?) {
val warnings = result.array("warnings")
if (warnings.isNotEmpty()) {
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text("Hinweise", fontWeight = FontWeight.Bold)
warnings.forEach { warning ->
@@ -825,7 +904,10 @@ private fun AnalysisVisuals(visuals: JsonObject) {
Text("Visualisierte Erkenntnisse", style = MaterialTheme.typography.titleMedium)
charts.forEach { chart ->
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(chart.string("title", "Verlauf"), fontWeight = FontWeight.Bold)
val series = chart.array("series").mapNotNull { it as? JsonObject }
@@ -838,7 +920,10 @@ private fun AnalysisVisuals(visuals: JsonObject) {
}
}
flowcharts.forEach { flow ->
OutlinedCard(Modifier.fillMaxWidth()) {
OutlinedElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(flow.string("title", "Progressionspfad"), fontWeight = FontWeight.Bold)
val nodes = flow.array("nodes").mapNotNull { it as? JsonObject }
@@ -853,12 +938,15 @@ private fun AnalysisVisuals(visuals: JsonObject) {
@Composable
private fun PlanPane(plan: PlanUi) {
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(plan.title, style = MaterialTheme.typography.headlineSmall)
Text(plan.subtitle)
Text("${plan.weeks} Wochen · veröffentlichte Revision ${plan.publishedRevision}")
plan.days.forEach { day ->
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Tag ${day.num}: ${day.focus}", style = MaterialTheme.typography.titleMedium)
day.rotations.forEach { rotation ->
@@ -875,9 +963,12 @@ private fun PlanPane(plan: PlanUi) {
private fun FaqPane(state: TrackerUiState) {
val plan = state.plan ?: return
val guide = state.response.obj("equivalence_guide")
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("FAQ & Variantencluster", style = MaterialTheme.typography.headlineSmall)
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Trainingsformat", style = MaterialTheme.typography.titleMedium)
Text("Modus: ${plan.trainingFormat.string("mode", "sets_reps")}")
@@ -887,7 +978,10 @@ private fun FaqPane(state: TrackerUiState) {
}
}
}
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Planbezogene Übungsbibliothek", style = MaterialTheme.typography.titleMedium)
plan.exerciseCatalog.values.mapNotNull { it as? JsonObject }.take(100).forEach { entry ->
@@ -896,7 +990,10 @@ private fun FaqPane(state: TrackerUiState) {
}
}
if (guide.isNotEmpty()) {
Card(Modifier.fillMaxWidth()) {
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Fallbackregeln des Servers", style = MaterialTheme.typography.titleMedium)
Text(guide.toString(), style = MaterialTheme.typography.bodySmall)
Vendored Executable → Regular
View File
View File