From 4490dd3126016e3300163c2f4032179fe9817df2 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 27 Jul 2026 08:03:29 +0200 Subject: [PATCH] Android UI --- .../de/drazz/boehmitools/training/ui/App.kt | 75 ++++---- .../training/ui/setup/SettingsScreen.kt | 98 +++++----- .../boehmitools/training/ui/theme/Theme.kt | 89 ++++++--- .../training/ui/tracker/TrackerScreen.kt | 169 ++++++++++++++---- android/gradlew | 0 android/tools/static_acceptance.py | 0 6 files changed, 293 insertions(+), 138 deletions(-) mode change 100755 => 100644 android/gradlew mode change 100755 => 100644 android/tools/static_acceptance.py diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt index 5aa9eb7..a5eab76 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt @@ -1,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,33 +42,47 @@ 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 { - destinations.forEach { destination -> - val selected = backStack?.destination?.hierarchy?.any { it.route == destination.route } == true - NavigationBarItem( - selected = selected, - onClick = { - navController.navigate(destination.route) { - popUpTo("tracker") { saveState = true } - launchSingleTop = true - restoreState = true - } - }, - icon = destination.icon, - label = { Text(destination.label) }, - ) + 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( + selected = selected, + onClick = { + navController.navigate(destination.route) { + popUpTo("tracker") { saveState = true } + launchSingleTop = true + restoreState = true + } + }, + 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") } }, ) } diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/setup/SettingsScreen.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/setup/SettingsScreen.kt index f68c859..6083405 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/setup/SettingsScreen.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/setup/SettingsScreen.kt @@ -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)) { - Text("Server & Anmeldung", style = MaterialTheme.typography.titleLarge) + 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)) { - Text("Darstellung", style = MaterialTheme.typography.titleLarge) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Icon(Icons.Default.Palette, null, tint = MaterialTheme.colorScheme.primary) + Text("Darstellung", style = MaterialTheme.typography.titleLarge) + } + 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)) diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt index 3c5a184..cc7a83d 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt @@ -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, + ) } diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt index 9ca2cd1..8efe4b0 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt @@ -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, 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, 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 -> - ExerciseCard(exercise, session, plan, vm) - HorizontalDivider() + 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) + } + } } } } @@ -197,7 +222,10 @@ private fun SessionPane(state: TrackerUiState, pending: List, v @Composable private fun PendingQueue(pending: List, 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, 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, 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, } 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, 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, } 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, } } - 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, @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) diff --git a/android/gradlew b/android/gradlew old mode 100755 new mode 100644 diff --git a/android/tools/static_acceptance.py b/android/tools/static_acceptance.py old mode 100755 new mode 100644