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 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.Icons
import androidx.compose.material.icons.filled.FitnessCenter 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.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton 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.unit.dp
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@@ -35,14 +42,24 @@ fun AppRoot(
) { ) {
val navController = rememberNavController() val navController = rememberNavController()
val destinations = listOf( val destinations = listOf(
AppDestination("tracker", "Tracker") { Icon(Icons.Default.FitnessCenter, null) }, AppDestination("tracker", "Training") { Icon(Icons.Default.FitnessCenter, null) },
AppDestination("settings", "Einstellungen") { Icon(Icons.Default.Settings, null) }, AppDestination("settings", "Mehr") { Icon(Icons.Default.Settings, null) },
) )
val backStack by navController.currentBackStackEntryAsState() val backStack by navController.currentBackStackEntryAsState()
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.background,
bottomBar = { 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 -> 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(
@@ -56,12 +73,16 @@ fun AppRoot(
}, },
icon = destination.icon, icon = destination.icon,
label = { Text(destination.label) }, label = { Text(destination.label) },
colors = NavigationBarItemDefaults.colors(
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
),
) )
} }
} }
}
}, },
) { padding -> ) { padding ->
NavHost(navController = navController, startDestination = "tracker", modifier = Modifier) { NavHost(navController = navController, startDestination = "tracker") {
composable("tracker") { TrackerScreen(contentPadding = padding) } composable("tracker") { TrackerScreen(contentPadding = padding) }
composable("settings") { composable("settings") {
SettingsScreen( SettingsScreen(
@@ -74,9 +95,7 @@ fun AppRoot(
} }
} }
if (showWhatsNew) { if (showWhatsNew) WhatsNewDialog(onDismiss = onDismissWhatsNew)
WhatsNewDialog(onDismiss = onDismissWhatsNew)
}
} }
@Composable @Composable
@@ -84,17 +103,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 = { Text("Moderner Card-Look, verbesserte Hierarchie und kompaktere Trainingseingaben. Alle bestehenden Daten- und Synchronisationsfunktionen bleiben erhalten.") },
Text( confirmButton = { TextButton(onClick = onDismiss) { Text("Verstanden") } },
"• 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") }
},
) )
} }
@@ -1,6 +1,8 @@
package de.drazz.boehmitools.training.ui.setup package de.drazz.boehmitools.training.ui.setup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row 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.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudDone import androidx.compose.material.icons.filled.CloudDone
import androidx.compose.material.icons.filled.Info 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.Button
import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -27,12 +33,17 @@ import androidx.compose.material3.Text
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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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.BuildConfig import de.drazz.boehmitools.training.BuildConfig
import de.drazz.boehmitools.training.data.settings.SettingsRepository 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 @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -48,65 +59,63 @@ fun SettingsScreen(
.fillMaxSize() .fillMaxSize()
.padding(contentPadding) .padding(contentPadding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(16.dp), .padding(horizontal = 14.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(16.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)) { 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("Server & Anmeldung", style = MaterialTheme.typography.titleLarge)
}
Text( 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, 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( OutlinedTextField(
value = state.serverUrl, state.password,
onValueChange = viewModel::updateServer, viewModel::updatePassword,
modifier = Modifier.fillMaxWidth(), 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(),
label = { Text("Passwort") }, label = { Text("Passwort") },
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
singleLine = true, singleLine = true,
) )
Button( Button(onClick = viewModel::saveAndTest, enabled = !state.loading, modifier = Modifier.fillMaxWidth()) {
onClick = viewModel::saveAndTest, if (state.loading) CircularProgressIndicator(modifier = Modifier.height(18.dp), strokeWidth = 2.dp)
enabled = !state.loading, else Icon(Icons.Default.CloudDone, null)
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)) Spacer(Modifier.width(8.dp))
Text("Speichern & Verbindung testen") 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) 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)) { 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) Text("Darstellung", style = MaterialTheme.typography.titleLarge)
Row( }
modifier = Modifier.fillMaxWidth(), Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
listOf("system" to "System", "light" to "Hell", "dark" to "Dunkel").forEach { (value, label) -> listOf("system" to "System", "light" to "Hell", "dark" to "Dunkel").forEach { (value, label) ->
FilterChip( FilterChip(
selected = current.theme == value, 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Über RipMe", style = MaterialTheme.typography.titleLarge) Text("Über RipMe", style = MaterialTheme.typography.titleLarge)
Text("Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") Text("Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
Text( Text("Native Android-App für deinen Trainingstracker.", color = MaterialTheme.colorScheme.onSurfaceVariant)
"Native Android-App für deinen Trainingstracker.",
style = MaterialTheme.typography.bodyMedium,
)
OutlinedButton(onClick = onShowWhatsNew, modifier = Modifier.fillMaxWidth()) { OutlinedButton(onClick = onShowWhatsNew, modifier = Modifier.fillMaxWidth()) {
Icon(Icons.Default.Info, null) Icon(Icons.Default.Info, null)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
@@ -1,39 +1,77 @@
package de.drazz.boehmitools.training.ui.theme package de.drazz.boehmitools.training.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
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.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( private val Light = lightColorScheme(
primary = Color(0xFF566600), primary = Color(0xFF6D3FE8),
onPrimary = Color.White, onPrimary = Color.White,
primaryContainer = Color(0xFFD9F26A), primaryContainer = Color(0xFFE9DDFF),
onPrimaryContainer = Color(0xFF172000), onPrimaryContainer = Color(0xFF22005D),
secondary = Color(0xFF53643C), secondary = Color(0xFF64558A),
tertiary = Color(0xFF00696C), onSecondary = Color.White,
background = Color(0xFFF9FAF2), secondaryContainer = Color(0xFFE9DEFF),
surface = Color(0xFFF9FAF2), onSecondaryContainer = Color(0xFF201047),
surfaceVariant = Color(0xFFE5E8D8), 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( private val Dark = darkColorScheme(
primary = Color(0xFFC0D852), primary = Color(0xFFCBB5FF),
onPrimary = Color(0xFF2B3400), onPrimary = Color(0xFF3B008F),
primaryContainer = Color(0xFF414D00), primaryContainer = Color(0xFF5427B6),
onPrimaryContainer = Color(0xFFD9F26A), onPrimaryContainer = Color(0xFFEADDFF),
secondary = Color(0xFFBACCA2), secondary = Color(0xFFCDC1E9),
tertiary = Color(0xFF80D4D6), onSecondary = Color(0xFF352A4F),
background = Color(0xFF11140E), secondaryContainer = Color(0xFF4C4167),
surface = Color(0xFF11140E), onSecondaryContainer = Color(0xFFE9DEFF),
surfaceVariant = Color(0xFF41443A), 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 @Composable
@@ -43,5 +81,10 @@ fun TrainingTheme(theme: String, content: @Composable () -> Unit) {
"light" -> false "light" -> false
else -> isSystemInDarkTheme() 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 package de.drazz.boehmitools.training.ui.tracker
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.height
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.material.icons.Icons 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.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.ElevatedCard
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
@@ -38,6 +42,7 @@ 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.Surface
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -49,10 +54,15 @@ 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.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.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.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
@@ -97,7 +107,7 @@ fun TrackerScreen(
TopAppBar( TopAppBar(
title = { title = {
Column { 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) if (state.saveState.isNotBlank()) Text(state.saveState, style = MaterialTheme.typography.labelSmall)
} }
}, },
@@ -121,7 +131,12 @@ fun TrackerScreen(
) { inner -> ) { inner ->
Column(Modifier.fillMaxSize().padding(inner)) { Column(Modifier.fillMaxSize().padding(inner)) {
val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ") 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) -> tabs.forEach { (id, label) ->
Tab( Tab(
selected = state.tab == id, 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 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(12.dp), Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) 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) SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm)
day.rotations.forEach { rotation -> 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)) { Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(rotation.label, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text(rotation.label, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
rotation.exercises.forEach { exercise -> 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) ExerciseCard(exercise, session, plan, vm)
HorizontalDivider() }
}
} }
} }
} }
@@ -197,7 +222,10 @@ private fun SessionPane(state: TrackerUiState, pending: List<PendingMutation>, v
@Composable @Composable
private fun PendingQueue(pending: List<PendingMutation>, vm: TrackerViewModel) { 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.CloudOff, null) Icon(Icons.Default.CloudOff, null)
@@ -276,11 +304,19 @@ private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: I
"completed" -> "Abgeschlossen" "completed" -> "Abgeschlossen"
else -> "Geplant" else -> "Geplant"
} }
Card(Modifier.fillMaxWidth()) { ElevatedCard(
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { modifier = Modifier.fillMaxWidth(),
Text("Woche $week · Tag ${day.num}", style = MaterialTheme.typography.labelLarge) elevation = CardDefaults.elevatedCardElevation(defaultElevation = 6.dp),
Text(day.focus, style = MaterialTheme.typography.headlineSmall) ) {
Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)} · Status: $statusLabel", style = MaterialTheme.typography.bodySmall) 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) { when (status) {
"in_progress" -> Row( "in_progress" -> Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -289,6 +325,10 @@ private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: I
Button( Button(
onClick = { vm.setSessionStatus("completed") }, onClick = { vm.setSessionStatus("completed") },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = BrandPurple,
),
) { ) {
Icon(Icons.Default.CheckCircle, null) Icon(Icons.Default.CheckCircle, null)
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
@@ -334,7 +374,10 @@ 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(Modifier.fillMaxWidth()) { ElevatedCard(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp),
) {
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.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 ->
@@ -362,14 +405,14 @@ private fun ExerciseCard(exercise: ExerciseUi, session: JsonObject, plan: PlanUi
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(exercise.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) Text(exercise.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
if (exercise.cue.isNotBlank()) Text(exercise.cue, style = MaterialTheme.typography.bodySmall) 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)) { Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Status", style = MaterialTheme.typography.labelMedium) Text("Status", style = MaterialTheme.typography.labelMedium)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp), 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( FilterChip(
selected = status == value, selected = status == value,
onClick = { vm.setItemStatus(exercise.id, value) }, onClick = { vm.setItemStatus(exercise.id, value) },
@@ -459,7 +502,10 @@ private fun ManualResultFormatEditor(
return JsonObject(values) 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
@@ -505,7 +551,10 @@ private fun ChoiceChips(label: String, choices: List<String>, selected: String,
@Composable @Composable
private fun ResultEditor(schema: ResultSchemaUi, data: JsonObject, onChange: (JsonObject) -> Unit) { 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Ergebnis · ${schema.source}", style = MaterialTheme.typography.labelLarge) Text("Ergebnis · ${schema.source}", style = MaterialTheme.typography.labelLarge)
Text("${schema.mode}, ${schema.laterality}, Gewicht ${schema.weightMode}", style = MaterialTheme.typography.bodySmall) 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( Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp), Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Text("Fortschritt", style = MaterialTheme.typography.headlineSmall) 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Lokale Übersicht", style = MaterialTheme.typography.titleMedium) Text("Lokale Übersicht", style = MaterialTheme.typography.titleMedium)
Text("$completed abgeschlossene Sessions · $stopped gestoppte Sessions · ${sessions.size} angelegte Sessions") 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) 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Auswertung auswählen", style = MaterialTheme.typography.titleMedium) Text("Auswertung auswählen", style = MaterialTheme.typography.titleMedium)
Row( Row(
@@ -645,7 +700,10 @@ private fun ProgressPane(state: TrackerUiState, pending: List<PendingMutation>,
} }
if (state.analysisScope == "week") { 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Wochenstatus", style = MaterialTheme.typography.titleMedium) Text("Wochenstatus", style = MaterialTheme.typography.titleMedium)
val closed = state.tracker.obj("week_statuses").obj(state.week.toString()).string("status") == "closed" 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("Manuelle Analyse", style = MaterialTheme.typography.titleMedium) Text("Manuelle Analyse", style = MaterialTheme.typography.titleMedium)
Button( Button(
@@ -697,7 +758,10 @@ private fun ProgressPane(state: TrackerUiState, pending: List<PendingMutation>,
@Composable @Composable
private fun AnalysisCard(title: String, record: JsonObject?) { 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium) Text(title, style = MaterialTheme.typography.titleMedium)
if (record == null) { 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")) val dataQuality = result.string("data_quality", result.obj("overview").string("data_quality"))
if (dataQuality.isNotBlank()) { 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Datenbasis", fontWeight = FontWeight.Bold) Text("Datenbasis", fontWeight = FontWeight.Bold)
Text(dataQuality) Text(dataQuality)
@@ -739,7 +806,10 @@ private fun AnalysisCard(title: String, record: JsonObject?) {
if (metrics.isNotEmpty()) { if (metrics.isNotEmpty()) {
Text("Kennzahlen", style = MaterialTheme.typography.titleMedium) Text("Kennzahlen", style = MaterialTheme.typography.titleMedium)
metrics.forEach { metric -> 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(metric.string("label"), style = MaterialTheme.typography.labelMedium) Text(metric.string("label"), style = MaterialTheme.typography.labelMedium)
Text(metric.string("value"), style = MaterialTheme.typography.titleMedium) Text(metric.string("value"), style = MaterialTheme.typography.titleMedium)
@@ -757,7 +827,10 @@ private fun AnalysisCard(title: String, record: JsonObject?) {
if (exercises.isNotEmpty()) { if (exercises.isNotEmpty()) {
Text("Übungsstand", style = MaterialTheme.typography.titleMedium) Text("Übungsstand", style = MaterialTheme.typography.titleMedium)
exercises.forEach { update -> 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(update.string("name", "Übung"), fontWeight = FontWeight.Bold) Text(update.string("name", "Übung"), fontWeight = FontWeight.Bold)
val trend = when (update.string("trend")) { val trend = when (update.string("trend")) {
@@ -785,7 +858,10 @@ private fun AnalysisCard(title: String, record: JsonObject?) {
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
adjustments.forEach { item -> 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text(item.string("target", "Vorschlag"), fontWeight = FontWeight.Bold) Text(item.string("target", "Vorschlag"), fontWeight = FontWeight.Bold)
if (item.string("suggested_change").isNotBlank()) Text(item.string("suggested_change")) 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") val warnings = result.array("warnings")
if (warnings.isNotEmpty()) { 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
Text("Hinweise", fontWeight = FontWeight.Bold) Text("Hinweise", fontWeight = FontWeight.Bold)
warnings.forEach { warning -> warnings.forEach { warning ->
@@ -825,7 +904,10 @@ private fun AnalysisVisuals(visuals: JsonObject) {
Text("Visualisierte Erkenntnisse", style = MaterialTheme.typography.titleMedium) Text("Visualisierte Erkenntnisse", style = MaterialTheme.typography.titleMedium)
charts.forEach { chart -> 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(chart.string("title", "Verlauf"), fontWeight = FontWeight.Bold) Text(chart.string("title", "Verlauf"), fontWeight = FontWeight.Bold)
val series = chart.array("series").mapNotNull { it as? JsonObject } val series = chart.array("series").mapNotNull { it as? JsonObject }
@@ -838,7 +920,10 @@ private fun AnalysisVisuals(visuals: JsonObject) {
} }
} }
flowcharts.forEach { flow -> 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)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(flow.string("title", "Progressionspfad"), fontWeight = FontWeight.Bold) Text(flow.string("title", "Progressionspfad"), fontWeight = FontWeight.Bold)
val nodes = flow.array("nodes").mapNotNull { it as? JsonObject } val nodes = flow.array("nodes").mapNotNull { it as? JsonObject }
@@ -853,12 +938,15 @@ private fun AnalysisVisuals(visuals: JsonObject) {
@Composable @Composable
private fun PlanPane(plan: PlanUi) { 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.title, style = MaterialTheme.typography.headlineSmall)
Text(plan.subtitle) Text(plan.subtitle)
Text("${plan.weeks} Wochen · veröffentlichte Revision ${plan.publishedRevision}") Text("${plan.weeks} Wochen · veröffentlichte Revision ${plan.publishedRevision}")
plan.days.forEach { day -> 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)) { Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Tag ${day.num}: ${day.focus}", style = MaterialTheme.typography.titleMedium) Text("Tag ${day.num}: ${day.focus}", style = MaterialTheme.typography.titleMedium)
day.rotations.forEach { rotation -> day.rotations.forEach { rotation ->
@@ -875,9 +963,12 @@ private fun PlanPane(plan: PlanUi) {
private fun FaqPane(state: TrackerUiState) { private fun FaqPane(state: TrackerUiState) {
val plan = state.plan ?: return val plan = state.plan ?: return
val guide = state.response.obj("equivalence_guide") 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) 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Trainingsformat", style = MaterialTheme.typography.titleMedium) Text("Trainingsformat", style = MaterialTheme.typography.titleMedium)
Text("Modus: ${plan.trainingFormat.string("mode", "sets_reps")}") 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Planbezogene Übungsbibliothek", style = MaterialTheme.typography.titleMedium) Text("Planbezogene Übungsbibliothek", style = MaterialTheme.typography.titleMedium)
plan.exerciseCatalog.values.mapNotNull { it as? JsonObject }.take(100).forEach { entry -> plan.exerciseCatalog.values.mapNotNull { it as? JsonObject }.take(100).forEach { entry ->
@@ -896,7 +990,10 @@ private fun FaqPane(state: TrackerUiState) {
} }
} }
if (guide.isNotEmpty()) { 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Fallbackregeln des Servers", style = MaterialTheme.typography.titleMedium) Text("Fallbackregeln des Servers", style = MaterialTheme.typography.titleMedium)
Text(guide.toString(), style = MaterialTheme.typography.bodySmall) Text(guide.toString(), style = MaterialTheme.typography.bodySmall)
Vendored Executable → Regular
View File
View File