Update
This commit is contained in:
@@ -511,3 +511,19 @@ fun resultDataWithSets(data: JsonObject, sets: Int): JsonObject {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun resultDataWithoutSet(data: JsonObject, index: Int): JsonObject {
|
||||
val currentCount = data.int("sets", 1).coerceIn(1, 20)
|
||||
if (currentCount <= 1 || index !in 0 until currentCount) return data
|
||||
|
||||
val values = data.toMutableMap()
|
||||
val newCount = currentCount - 1
|
||||
values["sets"] = JsonPrimitive(newCount)
|
||||
for (field in listOf("values", "left_values", "right_values")) {
|
||||
val list = data.array(field).toMutableList()
|
||||
if (index in list.indices) list.removeAt(index)
|
||||
while (list.size < newCount) list.add(JsonNull)
|
||||
values[field] = JsonArray(list.take(newCount))
|
||||
}
|
||||
return JsonObject(values)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
package de.drazz.boehmitools.training.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Analytics
|
||||
import androidx.compose.material.icons.filled.FitnessCenter
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
@@ -45,64 +51,106 @@ fun AppRoot(
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val destinations = listOf(
|
||||
AppDestination("tracker", "Tracking") { Icon(Icons.Default.FitnessCenter, null) },
|
||||
AppDestination("settings", "Mehr") { Icon(Icons.Default.Settings, null) },
|
||||
AppDestination("plan", "Plan") { Icon(Icons.Default.Home, null) },
|
||||
AppDestination("tracking", "Tracking") { Icon(Icons.Default.FitnessCenter, null) },
|
||||
AppDestination("analysis", "Analyse") { Icon(Icons.Default.Analytics, null) },
|
||||
AppDestination("more", "Mehr") { Icon(Icons.Default.Settings, null) },
|
||||
)
|
||||
val backStack by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = backStack?.destination?.route ?: "tracking"
|
||||
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
bottomBar = {
|
||||
Box(Modifier.padding(horizontal = 18.dp, vertical = 12.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = Color.Transparent,
|
||||
bottomBar = {
|
||||
CompactBottomBar(destinations, currentRoute) { route ->
|
||||
navController.navigate(route) {
|
||||
popUpTo(navController.graph.startDestinationId) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
NavHost(navController = navController, startDestination = "tracking") {
|
||||
composable("plan") { TrackerScreen(contentPadding = paddedContent(padding), initialTab = "plan") }
|
||||
composable("tracking") { TrackerScreen(contentPadding = paddedContent(padding), initialTab = "session") }
|
||||
composable("analysis") { TrackerScreen(contentPadding = paddedContent(padding), initialTab = "progress") }
|
||||
composable("more") {
|
||||
SettingsScreen(
|
||||
contentPadding = paddedContent(padding),
|
||||
current = settings,
|
||||
onTheme = onTheme,
|
||||
onShowWhatsNew = onShowWhatsNew,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showWhatsNew) {
|
||||
WhatsNewDialog(onDismiss = onDismissWhatsNew)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun paddedContent(original: PaddingValues): PaddingValues = PaddingValues(
|
||||
top = original.calculateTopPadding(),
|
||||
bottom = original.calculateBottomPadding(),
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun CompactBottomBar(
|
||||
destinations: List<AppDestination>,
|
||||
currentRoute: String,
|
||||
onNavigate: (String) -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp,
|
||||
shadowElevation = 6.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
destinations.forEach { destination ->
|
||||
val selected = currentRoute == destination.route
|
||||
Surface(
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shadowElevation = 14.dp,
|
||||
tonalElevation = 4.dp,
|
||||
onClick = { onNavigate(destination.route) },
|
||||
modifier = Modifier.weight(1f),
|
||||
color = Color.Transparent,
|
||||
) {
|
||||
NavigationBar(
|
||||
containerColor = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
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,
|
||||
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.16f) else Color.Transparent),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
destination.icon()
|
||||
}
|
||||
Text(
|
||||
destination.label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
NavHost(navController = navController, startDestination = "tracker", modifier = Modifier.fillMaxSize()) {
|
||||
composable("tracker") { TrackerScreen(contentPadding = padding) }
|
||||
composable("settings") {
|
||||
SettingsScreen(
|
||||
contentPadding = padding,
|
||||
current = settings,
|
||||
onTheme = onTheme,
|
||||
onShowWhatsNew = onShowWhatsNew,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showWhatsNew) WhatsNewDialog(onDismiss = onDismissWhatsNew)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -110,7 +158,14 @@ private fun WhatsNewDialog(onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Neu in RipMe ${BuildConfig.VERSION_NAME}") },
|
||||
text = { Text("Modern Card UI, sicherer Offline-Tracker und vollständige Analyseansichten.") },
|
||||
text = {
|
||||
Text(
|
||||
"• RipMe konzentriert sich vollständig auf den Trainingstracker.\n" +
|
||||
"• Analyseergebnisse werden jetzt vollständig in der App angezeigt.\n" +
|
||||
"• Wochen und Trainingstage zeigen ihren aktuellen Abschlussstatus.\n" +
|
||||
"• Offline erfasste Sessionänderungen werden weiterhin sicher synchronisiert.",
|
||||
)
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text("Verstanden") } },
|
||||
)
|
||||
}
|
||||
|
||||
+49
-75
@@ -2,6 +2,7 @@ package de.drazz.boehmitools.training.ui.setup
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -9,24 +10,27 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@@ -49,90 +53,60 @@ fun SettingsScreen(
|
||||
.padding(contentPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
Text("RipMe Einstellungen", style = MaterialTheme.typography.headlineMedium)
|
||||
Text("Mehr", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Text("Einstellungen & App", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
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.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
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(),
|
||||
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)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Speichern & Verbindung testen")
|
||||
SettingsCard(title = "Server & Anmeldung", subtitle = "Zugangsdaten werden lokal verschlüsselt gespeichert.") {
|
||||
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(), label = { Text("Passwort") }, visualTransformation = PasswordVisualTransformation(), singleLine = true)
|
||||
Button(onClick = viewModel::saveAndTest, enabled = !state.loading, modifier = Modifier.fillMaxWidth()) {
|
||||
if (state.loading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Icon(Icons.Default.CloudDone, null)
|
||||
}
|
||||
if (state.message.isNotBlank()) Text(state.message, color = MaterialTheme.colorScheme.primary)
|
||||
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text("Speichern & Verbindung testen")
|
||||
}
|
||||
if (state.message.isNotBlank()) Text(state.message, color = MaterialTheme.colorScheme.primary)
|
||||
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
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),
|
||||
) {
|
||||
listOf("system" to "System", "light" to "Hell", "dark" to "Dunkel").forEach { (value, label) ->
|
||||
FilterChip(
|
||||
selected = current.theme == value,
|
||||
onClick = { onTheme(value) },
|
||||
label = { Text(label, maxLines = 1) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
SettingsCard(title = "Darstellung", subtitle = "Modern Card Style in Hell, Dunkel oder System.") {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf("system" to "System", "light" to "Hell", "dark" to "Dunkel").forEach { (value, label) ->
|
||||
FilterChip(selected = current.theme == value, onClick = { onTheme(value) }, label = { Text(label) }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
OutlinedButton(onClick = onShowWhatsNew, modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(Icons.Default.Info, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Was ist neu?")
|
||||
}
|
||||
SettingsCard(title = "Über RipMe", subtitle = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") {
|
||||
Text("Native Android-App für deinen Trainingstracker im Modern-Card-Stil.")
|
||||
OutlinedButton(onClick = onShowWhatsNew, modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(Icons.Default.Info, null)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text("Was ist neu?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsCard(title: String, subtitle: String, content: @Composable ColumnScope.() -> Unit) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp,
|
||||
shadowElevation = 8.dp,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp), content = {
|
||||
Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
content()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,66 @@
|
||||
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.unit.dp
|
||||
|
||||
private val Light = lightColorScheme(
|
||||
primary = Color(0xFF7C3AED),
|
||||
primary = Color(0xFF8E56FF),
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Color(0xFFEDE9FE),
|
||||
onPrimaryContainer = Color(0xFF2E1065),
|
||||
secondary = Color(0xFF4F46E5),
|
||||
primaryContainer = Color(0xFFEDE3FF),
|
||||
onPrimaryContainer = Color(0xFF2B1459),
|
||||
secondary = Color(0xFF6A7086),
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = Color(0xFFE0E7FF),
|
||||
tertiary = Color(0xFF06B6D4),
|
||||
background = Color(0xFFF4F5FB),
|
||||
onBackground = Color(0xFF171725),
|
||||
secondaryContainer = Color(0xFFF0F2F8),
|
||||
onSecondaryContainer = Color(0xFF1C2132),
|
||||
tertiary = Color(0xFFAC8BFF),
|
||||
background = Color(0xFFF5F6FB),
|
||||
onBackground = Color(0xFF171923),
|
||||
surface = Color(0xFFFFFFFF),
|
||||
onSurface = Color(0xFF171725),
|
||||
surfaceVariant = Color(0xFFF0F1F7),
|
||||
onSurfaceVariant = Color(0xFF69697A),
|
||||
outline = Color(0xFFD9DAE5),
|
||||
onSurface = Color(0xFF171923),
|
||||
surfaceVariant = Color(0xFFF1F2F8),
|
||||
onSurfaceVariant = Color(0xFF666B7B),
|
||||
outline = Color(0xFFDEE2EE),
|
||||
outlineVariant = Color(0xFFE9ECF6),
|
||||
error = Color(0xFFC54646),
|
||||
onError = Color.White,
|
||||
)
|
||||
|
||||
private val Dark = darkColorScheme(
|
||||
primary = Color(0xFFA78BFA),
|
||||
onPrimary = Color(0xFF24005B),
|
||||
primaryContainer = Color(0xFF4C1D95),
|
||||
onPrimaryContainer = Color(0xFFEDE9FE),
|
||||
secondary = Color(0xFF818CF8),
|
||||
onSecondary = Color(0xFF10144A),
|
||||
secondaryContainer = Color(0xFF312E81),
|
||||
tertiary = Color(0xFF22D3EE),
|
||||
background = Color(0xFF0B0D17),
|
||||
onBackground = Color(0xFFF5F5FA),
|
||||
surface = Color(0xFF151824),
|
||||
onSurface = Color(0xFFF5F5FA),
|
||||
surfaceVariant = Color(0xFF202432),
|
||||
onSurfaceVariant = Color(0xFFB8BBC8),
|
||||
outline = Color(0xFF343849),
|
||||
primary = Color(0xFFA66BFF),
|
||||
onPrimary = Color(0xFF1D0C45),
|
||||
primaryContainer = Color(0xFF2A175B),
|
||||
onPrimaryContainer = Color(0xFFEFE7FF),
|
||||
secondary = Color(0xFFAAB1C8),
|
||||
onSecondary = Color(0xFF1B2236),
|
||||
secondaryContainer = Color(0xFF1C2235),
|
||||
onSecondaryContainer = Color(0xFFDEE3F7),
|
||||
tertiary = Color(0xFFCC9DFF),
|
||||
background = Color(0xFF0B1020),
|
||||
onBackground = Color(0xFFF3F5FB),
|
||||
surface = Color(0xFF161C2D),
|
||||
onSurface = Color(0xFFF3F5FB),
|
||||
surfaceVariant = Color(0xFF21283C),
|
||||
onSurfaceVariant = Color(0xFFB1B8CD),
|
||||
outline = Color(0xFF363D55),
|
||||
outlineVariant = Color(0xFF272E45),
|
||||
error = Color(0xFFFF8A8A),
|
||||
onError = Color(0xFF320F16),
|
||||
)
|
||||
|
||||
private val AppShapes = Shapes(
|
||||
extraSmall = RoundedCornerShape(12.dp),
|
||||
small = RoundedCornerShape(18.dp),
|
||||
medium = RoundedCornerShape(24.dp),
|
||||
large = RoundedCornerShape(28.dp),
|
||||
extraLarge = RoundedCornerShape(32.dp),
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -50,5 +70,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 = Typography(),
|
||||
shapes = AppShapes,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
+812
-593
File diff suppressed because it is too large
Load Diff
+91
@@ -0,0 +1,91 @@
|
||||
package de.drazz.boehmitools.training.domain
|
||||
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ResultDataEditingTest {
|
||||
@Test
|
||||
fun addingSetPreservesAllExistingValuesAndWeight() {
|
||||
val original = buildJsonObject {
|
||||
put("version", 2)
|
||||
put("mode", "reps")
|
||||
put("laterality", "bilateral")
|
||||
put("sides_mode", "same")
|
||||
put("sets", 3)
|
||||
put("weight_kg", 80.0)
|
||||
put("values", buildJsonArray {
|
||||
add(8.0)
|
||||
add(7.0)
|
||||
add(6.0)
|
||||
})
|
||||
put("left_values", buildJsonArray { repeat(3) { add(JsonNull) } })
|
||||
put("right_values", buildJsonArray { repeat(3) { add(JsonNull) } })
|
||||
}
|
||||
|
||||
val expanded = resultDataWithSets(original, 4)
|
||||
|
||||
assertEquals(4, expanded.int("sets"))
|
||||
assertEquals(80.0, expanded["weight_kg"]!!.jsonPrimitive.doubleOrNull!!, 0.0)
|
||||
assertEquals(listOf(8.0, 7.0, 6.0), expanded.array("values").take(3).map { it.jsonPrimitive.doubleOrNull })
|
||||
assertEquals(JsonNull, expanded.array("values")[3])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removingMiddleSetKeepsRemainingValuesInOrder() {
|
||||
val original = buildJsonObject {
|
||||
put("version", 2)
|
||||
put("mode", "reps")
|
||||
put("laterality", "bilateral")
|
||||
put("sides_mode", "same")
|
||||
put("sets", 3)
|
||||
put("weight_kg", JsonPrimitive(85.0))
|
||||
put("values", buildJsonArray {
|
||||
add(10.0)
|
||||
add(8.0)
|
||||
add(6.0)
|
||||
})
|
||||
put("left_values", buildJsonArray { repeat(3) { add(JsonNull) } })
|
||||
put("right_values", buildJsonArray { repeat(3) { add(JsonNull) } })
|
||||
}
|
||||
|
||||
val reduced = resultDataWithoutSet(original, 1)
|
||||
|
||||
assertEquals(2, reduced.int("sets"))
|
||||
assertEquals(listOf(10.0, 6.0), reduced.array("values").map { it.jsonPrimitive.doubleOrNull })
|
||||
assertEquals(85.0, reduced["weight_kg"]!!.jsonPrimitive.doubleOrNull!!, 0.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removingUnilateralSetPreservesBothSides() {
|
||||
val original = buildJsonObject {
|
||||
put("version", 2)
|
||||
put("mode", "seconds")
|
||||
put("laterality", "unilateral")
|
||||
put("sides_mode", "separate")
|
||||
put("sets", 3)
|
||||
put("weight_kg", JsonNull)
|
||||
put("values", buildJsonArray { repeat(3) { add(JsonNull) } })
|
||||
put("left_values", buildJsonArray {
|
||||
add(20.0)
|
||||
add(18.0)
|
||||
add(16.0)
|
||||
})
|
||||
put("right_values", buildJsonArray {
|
||||
add(19.0)
|
||||
add(17.0)
|
||||
add(15.0)
|
||||
})
|
||||
}
|
||||
|
||||
val reduced = resultDataWithoutSet(original, 0)
|
||||
|
||||
assertEquals(listOf(18.0, 16.0), reduced.array("left_values").map { it.jsonPrimitive.doubleOrNull })
|
||||
assertEquals(listOf(17.0, 15.0), reduced.array("right_values").map { it.jsonPrimitive.doubleOrNull })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user