From e905ec5bb81061820e7b8469f3468ae6776fa2bf Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 27 Jul 2026 12:16:02 +0200 Subject: [PATCH] UI --- .../de/drazz/boehmitools/training/ui/App.kt | 61 +++-- .../training/ui/setup/SettingsScreen.kt | 98 ++++--- .../boehmitools/training/ui/theme/Theme.kt | 93 +++---- .../training/ui/tracker/TrackerScreen.kt | 248 ++++++------------ android/gradle.properties | 1 - android/gradlew | 0 6 files changed, 197 insertions(+), 304 deletions(-) mode change 100755 => 100644 android/gradlew 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 a5eab76..ed8b8e1 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,5 +1,7 @@ package de.drazz.boehmitools.training.ui +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -7,9 +9,7 @@ import androidx.compose.material.icons.filled.FitnessCenter import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold @@ -42,41 +42,40 @@ fun AppRoot( ) { val navController = rememberNavController() val destinations = listOf( - AppDestination("tracker", "Training") { Icon(Icons.Default.FitnessCenter, null) }, + AppDestination("tracker", "Tracking") { Icon(Icons.Default.FitnessCenter, null) }, AppDestination("settings", "Mehr") { Icon(Icons.Default.Settings, null) }, ) val backStack by navController.currentBackStackEntryAsState() Scaffold( - containerColor = MaterialTheme.colorScheme.background, + containerColor = androidx.compose.material3.MaterialTheme.colorScheme.background, bottomBar = { - 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, + Box(Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp)) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + tonalElevation = 10.dp, + shadowElevation = 12.dp, ) { - 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, - ), - ) + NavigationBar(containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface) { + 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 = androidx.compose.material3.MaterialTheme.colorScheme.primaryContainer, + ), + ) + } } } } @@ -103,7 +102,7 @@ private fun WhatsNewDialog(onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, title = { Text("Neu in RipMe ${BuildConfig.VERSION_NAME}") }, - text = { Text("Moderner Card-Look, verbesserte Hierarchie und kompaktere Trainingseingaben. Alle bestehenden Daten- und Synchronisationsfunktionen bleiben erhalten.") }, + text = { Text("Modern Card Design, klarere Trainingseingaben und eine kompaktere Navigation.") }, 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 6083405..f68c859 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,8 +1,6 @@ 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 @@ -13,17 +11,13 @@ 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.CardDefaults +import androidx.compose.material3.Card 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 @@ -33,17 +27,12 @@ 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( @@ -59,63 +48,65 @@ fun SettingsScreen( .fillMaxSize() .padding(contentPadding) .verticalScroll(rememberScrollState()) - .padding(horizontal = 14.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - 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)) - } - } + Text("RipMe Einstellungen", style = MaterialTheme.typography.headlineMedium) - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { 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( - "Zugangsdaten werden lokal mit Android-Keystore-Unterstützung geschützt. Release-Builds akzeptieren ausschließlich HTTPS.", - color = MaterialTheme.colorScheme.onSurfaceVariant, + "Basic-Auth-Zugangsdaten werden mit einem Android-Keystore-Schlüssel verschlüsselt. Release-Builds akzeptieren ausschließlich HTTPS.", 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( - state.password, - viewModel::updatePassword, - Modifier.fillMaxWidth(), + 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(), 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.tertiary) + if (state.message.isNotBlank()) Text(state.message, color = MaterialTheme.colorScheme.primary) if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) } } - ElevatedCard(Modifier.fillMaxWidth(), elevation = CardDefaults.elevatedCardElevation(3.dp)) { + Card(Modifier.fillMaxWidth()) { 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.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Text("Darstellung", style = MaterialTheme.typography.titleLarge) + Row( + modifier = 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, @@ -128,11 +119,14 @@ fun SettingsScreen( } } - ElevatedCard(Modifier.fillMaxWidth(), elevation = CardDefaults.elevatedCardElevation(3.dp)) { + Card(Modifier.fillMaxWidth()) { 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.", color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + "Native Android-App für deinen Trainingstracker.", + style = MaterialTheme.typography.bodyMedium, + ) 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 cc7a83d..0c24d82 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,75 +1,63 @@ package de.drazz.boehmitools.training.ui.theme 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.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color 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) +val ModernPurple = Color(0xFF7C3AED) +val ModernViolet = Color(0xFF9B5CF6) +val ModernIndigo = Color(0xFF4F46E5) +val ModernGreen = Color(0xFF22C55E) private val Light = lightColorScheme( - primary = Color(0xFF6D3FE8), + primary = ModernPurple, onPrimary = Color.White, - primaryContainer = Color(0xFFE9DDFF), - onPrimaryContainer = Color(0xFF22005D), - secondary = Color(0xFF64558A), + primaryContainer = Color(0xFFEDE9FE), + onPrimaryContainer = Color(0xFF32136B), + secondary = ModernIndigo, 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), + secondaryContainer = Color(0xFFE0E7FF), + onSecondaryContainer = Color(0xFF1E1B4B), + tertiary = ModernGreen, + background = Color(0xFFF6F5FA), + onBackground = Color(0xFF17151D), + surface = Color(0xFFFFFFFF), + onSurface = Color(0xFF17151D), + surfaceVariant = Color(0xFFF0EEF5), + onSurfaceVariant = Color(0xFF66616F), + outline = Color(0xFFDDD8E7), + outlineVariant = Color(0xFFEAE6F0), ) private val Dark = darkColorScheme( - 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), + primary = Color(0xFFA78BFA), + onPrimary = Color(0xFF221047), + primaryContainer = Color(0xFF4C1D95), + onPrimaryContainer = Color(0xFFF3E8FF), + secondary = Color(0xFF818CF8), + onSecondary = Color(0xFF11133F), + secondaryContainer = Color(0xFF312E81), + onSecondaryContainer = Color(0xFFE0E7FF), + tertiary = Color(0xFF34D399), + background = Color(0xFF0D111A), + onBackground = Color(0xFFF5F3FA), + surface = Color(0xFF151A25), + onSurface = Color(0xFFF5F3FA), + surfaceVariant = Color(0xFF202634), + onSurfaceVariant = Color(0xFFB7B2C3), + outline = Color(0xFF383F50), + outlineVariant = Color(0xFF282E3B), ) -private val 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), +private val ModernTypography = Typography().copy( + headlineSmall = Typography().headlineSmall.copy(fontWeight = FontWeight.Bold, letterSpacing = (-0.4).sp), + titleLarge = Typography().titleLarge.copy(fontWeight = FontWeight.Bold), titleMedium = Typography().titleMedium.copy(fontWeight = FontWeight.SemiBold), labelLarge = Typography().labelLarge.copy(fontWeight = FontWeight.SemiBold), ) @@ -83,8 +71,7 @@ fun TrainingTheme(theme: String, content: @Composable () -> Unit) { } MaterialTheme( colorScheme = if (dark) Dark else Light, - typography = AppTypography, - shapes = AppShapes, + typography = ModernTypography, 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 9a645e0..8152c91 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 @@ -11,11 +11,15 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.MoreVert @@ -27,7 +31,6 @@ 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 @@ -42,8 +45,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.PrimaryTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -54,15 +56,13 @@ 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 de.drazz.boehmitools.training.ui.theme.ModernIndigo +import de.drazz.boehmitools.training.ui.theme.ModernPurple +import de.drazz.boehmitools.training.ui.theme.ModernViolet import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.drazz.boehmitools.training.data.local.PendingMutation @@ -103,11 +103,12 @@ fun TrackerScreen( Scaffold( modifier = Modifier.padding(contentPadding), + containerColor = MaterialTheme.colorScheme.background, topBar = { TopAppBar( title = { Column { - Text(state.plan?.title ?: "RipMe", maxLines = 1, style = MaterialTheme.typography.titleLarge) + Text(state.plan?.title ?: "RipMe", maxLines = 2) if (state.saveState.isNotBlank()) Text(state.saveState, style = MaterialTheme.typography.labelSmall) } }, @@ -131,11 +132,9 @@ fun TrackerScreen( ) { inner -> Column(Modifier.fillMaxSize().padding(inner)) { val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ") - TabRow( + PrimaryTabRow( selectedTabIndex = tabs.indexOfFirst { it.first == state.tab }.coerceAtLeast(0), containerColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.primary, - divider = {}, ) { tabs.forEach { (id, label) -> Tab( @@ -185,7 +184,7 @@ private fun SessionPane(state: TrackerUiState, pending: List, v val session = trackerSession(state.tracker, de.drazz.boehmitools.training.domain.sessionKey(state.week, state.day), plan) Column( Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), ) { if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) if (pending.isNotEmpty()) PendingQueue(pending, vm) @@ -194,22 +193,16 @@ private fun SessionPane(state: TrackerUiState, pending: List, v SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm) day.rotations.forEach { rotation -> - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { - Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 5.dp), + ) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { Text(rotation.label, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) rotation.exercises.forEach { exercise -> - 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) } } } @@ -222,10 +215,7 @@ private fun SessionPane(state: TrackerUiState, pending: List, v @Composable private fun PendingQueue(pending: List, vm: TrackerViewModel) { - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(Icons.Default.CloudOff, null) @@ -304,69 +294,34 @@ private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: I "completed" -> "Abgeschlossen" else -> "Geplant" } - ElevatedCard( + Card( modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 6.dp), + shape = RoundedCornerShape(24.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp), ) { Column( Modifier - .background(Brush.linearGradient(listOf(BrandIndigo, BrandPurple, BrandViolet))) - .padding(18.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), + .background(Brush.linearGradient(listOf(ModernPurple, ModernIndigo))) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Text("Woche $week · Tag ${day.num}", color = Color.White.copy(alpha = 0.86f), style = MaterialTheme.typography.labelLarge) + Text("Woche $week · Tag ${day.num}", color = Color.White.copy(alpha = .8f), style = MaterialTheme.typography.labelLarge) Text(day.focus, color = Color.White, style = MaterialTheme.typography.headlineSmall) - Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)} · Status: $statusLabel", color = Color.White.copy(alpha = 0.78f), style = MaterialTheme.typography.bodySmall) + Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)} · $statusLabel", color = Color.White.copy(alpha = .78f), style = MaterialTheme.typography.bodySmall) when (status) { - "in_progress" -> Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - 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)) - Text("Abschließen", maxLines = 1) + "in_progress" -> Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Button(onClick = { vm.setSessionStatus("completed") }, modifier = Modifier.weight(1f)) { + Icon(Icons.Default.CheckCircle, null); Spacer(Modifier.width(6.dp)); Text("Abschließen", maxLines = 1) } - OutlinedButton( - onClick = { vm.setSessionStatus("stopped") }, - modifier = Modifier.weight(1f), - ) { - Icon(Icons.Default.Stop, null) - Spacer(Modifier.width(6.dp)) - Text("Stoppen", maxLines = 1) + OutlinedButton(onClick = { vm.setSessionStatus("stopped") }, modifier = Modifier.weight(1f)) { + Icon(Icons.Default.Stop, null); Spacer(Modifier.width(6.dp)); Text("Stoppen", maxLines = 1) } } - "stopped" -> Button( - onClick = { vm.setSessionStatus("in_progress") }, - modifier = Modifier.fillMaxWidth(), - ) { - Icon(Icons.Default.PlayArrow, null) - Spacer(Modifier.width(6.dp)) - Text("Session fortsetzen") - } - "completed" -> OutlinedButton( - onClick = { vm.setSessionStatus("in_progress") }, - modifier = Modifier.fillMaxWidth(), - ) { Text("Session wieder öffnen") } - else -> Button( - onClick = { vm.setSessionStatus("in_progress") }, - modifier = Modifier.fillMaxWidth(), - ) { - Icon(Icons.Default.PlayArrow, null) - Spacer(Modifier.width(6.dp)) - Text("Session starten") - } - } - TextButton(onClick = vm::resetSession, modifier = Modifier.fillMaxWidth()) { - Text("Session zurücksetzen") + "stopped" -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.PlayArrow, null); Spacer(Modifier.width(6.dp)); Text("Session fortsetzen") } + "completed" -> OutlinedButton(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session wieder öffnen") } + else -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.PlayArrow, null); Spacer(Modifier.width(6.dp)); Text("Session starten") } } + TextButton(onClick = vm::resetSession, modifier = Modifier.fillMaxWidth()) { Text("Session zurücksetzen", color = Color.White.copy(alpha = .82f)) } } } } @@ -374,11 +329,12 @@ 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 - ElevatedCard( + Card( modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), + shape = RoundedCornerShape(20.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), ) { - Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) items.forEachIndexed { index, label -> val id = "$prefix-$index" @@ -402,17 +358,28 @@ private fun ExerciseCard(exercise: ExerciseUi, session: JsonObject, plan: PlanUi val data = normalizedResultData(item, schema) var progressionMenu by remember(exercise.id, progression) { mutableStateOf(false) } - 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("Übungs-ID · ${exercise.id}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.labelSmall) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(18.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = .72f)), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + ) { + Column(Modifier.fillMaxWidth().padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Column(Modifier.weight(1f)) { + Text(exercise.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + if (exercise.cue.isNotBlank()) Text(exercise.cue, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Box(Modifier.size(32.dp).clip(RoundedCornerShape(10.dp)).background(MaterialTheme.colorScheme.primaryContainer)) + } + Text("ID ${exercise.id}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { Text("Status", style = MaterialTheme.typography.labelMedium) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - listOf("planned" to "Offen", "completed" to "Fertig", "partial" to "Teilw.", "skipped" to "Skip").forEach { (value, label) -> + listOf("planned" to "Offen", "completed" to "Erledigt", "partial" to "Teilweise", "skipped" to "Übersprungen").forEach { (value, label) -> FilterChip( selected = status == value, onClick = { vm.setItemStatus(exercise.id, value) }, @@ -469,6 +436,7 @@ private fun ExerciseCard(exercise: ExerciseUi, session: JsonObject, plan: PlanUi ) } } + } } @@ -502,10 +470,7 @@ private fun ManualResultFormatEditor( return JsonObject(values) } - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Column(Modifier.weight(1f)) { @@ -551,10 +516,7 @@ private fun ChoiceChips(label: String, choices: List, selected: String, @Composable private fun ResultEditor(schema: ResultSchemaUi, data: JsonObject, onChange: (JsonObject) -> Unit) { - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { 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) @@ -651,15 +613,12 @@ private fun ProgressPane(state: TrackerUiState, pending: List, } Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp), + Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text("Fortschritt", style = MaterialTheme.typography.headlineSmall) - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { 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") @@ -669,10 +628,7 @@ private fun ProgressPane(state: TrackerUiState, pending: List, if (pending.isNotEmpty()) PendingQueue(pending, vm) - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Text("Auswertung auswählen", style = MaterialTheme.typography.titleMedium) Row( @@ -700,10 +656,7 @@ private fun ProgressPane(state: TrackerUiState, pending: List, } if (state.analysisScope == "week") { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { 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" @@ -721,10 +674,7 @@ private fun ProgressPane(state: TrackerUiState, pending: List, } } - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Text("Manuelle Analyse", style = MaterialTheme.typography.titleMedium) Button( @@ -758,10 +708,7 @@ private fun ProgressPane(state: TrackerUiState, pending: List, @Composable private fun AnalysisCard(title: String, record: JsonObject?) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { Text(title, style = MaterialTheme.typography.titleMedium) if (record == null) { @@ -791,10 +738,7 @@ private fun AnalysisCard(title: String, record: JsonObject?) { val dataQuality = result.string("data_quality", result.obj("overview").string("data_quality")) if (dataQuality.isNotBlank()) { - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { Text("Datenbasis", fontWeight = FontWeight.Bold) Text(dataQuality) @@ -806,10 +750,7 @@ private fun AnalysisCard(title: String, record: JsonObject?) { if (metrics.isNotEmpty()) { Text("Kennzahlen", style = MaterialTheme.typography.titleMedium) metrics.forEach { metric -> - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { 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) @@ -827,10 +768,7 @@ private fun AnalysisCard(title: String, record: JsonObject?) { if (exercises.isNotEmpty()) { Text("Übungsstand", style = MaterialTheme.typography.titleMedium) exercises.forEach { update -> - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { Text(update.string("name", "Übung"), fontWeight = FontWeight.Bold) val trend = when (update.string("trend")) { @@ -858,10 +796,7 @@ private fun AnalysisCard(title: String, record: JsonObject?) { style = MaterialTheme.typography.bodySmall, ) adjustments.forEach { item -> - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { 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")) @@ -875,10 +810,7 @@ private fun AnalysisCard(title: String, record: JsonObject?) { val warnings = result.array("warnings") if (warnings.isNotEmpty()) { - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { Text("Hinweise", fontWeight = FontWeight.Bold) warnings.forEach { warning -> @@ -904,10 +836,7 @@ private fun AnalysisVisuals(visuals: JsonObject) { Text("Visualisierte Erkenntnisse", style = MaterialTheme.typography.titleMedium) charts.forEach { chart -> - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { 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 } @@ -920,10 +849,7 @@ private fun AnalysisVisuals(visuals: JsonObject) { } } flowcharts.forEach { flow -> - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + OutlinedCard(Modifier.fillMaxWidth()) { 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 } @@ -938,15 +864,12 @@ private fun AnalysisVisuals(visuals: JsonObject) { @Composable private fun PlanPane(plan: PlanUi) { - Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(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 -> - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("Tag ${day.num}: ${day.focus}", style = MaterialTheme.typography.titleMedium) day.rotations.forEach { rotation -> @@ -963,12 +886,9 @@ 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(horizontal = 14.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { Text("FAQ & Variantencluster", style = MaterialTheme.typography.headlineSmall) - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Text("Trainingsformat", style = MaterialTheme.typography.titleMedium) Text("Modus: ${plan.trainingFormat.string("mode", "sets_reps")}") @@ -978,10 +898,7 @@ private fun FaqPane(state: TrackerUiState) { } } } - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { 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 -> @@ -990,10 +907,7 @@ private fun FaqPane(state: TrackerUiState) { } } if (guide.isNotEmpty()) { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 3.dp), - ) { + Card(Modifier.fillMaxWidth()) { 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/gradle.properties b/android/gradle.properties index dd579ce..f97184b 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -2,4 +2,3 @@ org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 android.useAndroidX=true kotlin.code.style=official android.nonTransitiveRClass=true -kapt.use.k2=false diff --git a/android/gradlew b/android/gradlew old mode 100755 new mode 100644