UI
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
package de.drazz.boehmitools.training.ui
|
package de.drazz.boehmitools.training.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -9,6 +10,7 @@ import androidx.compose.material.icons.filled.FitnessCenter
|
|||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.NavigationBarItemDefaults
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
@@ -19,6 +21,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@@ -48,16 +51,19 @@ fun AppRoot(
|
|||||||
val backStack by navController.currentBackStackEntryAsState()
|
val backStack by navController.currentBackStackEntryAsState()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Box(Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp)) {
|
Box(Modifier.padding(horizontal = 18.dp, vertical = 12.dp)) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
shape = RoundedCornerShape(28.dp),
|
||||||
shape = RoundedCornerShape(24.dp),
|
color = MaterialTheme.colorScheme.surface,
|
||||||
tonalElevation = 10.dp,
|
shadowElevation = 14.dp,
|
||||||
shadowElevation = 12.dp,
|
tonalElevation = 4.dp,
|
||||||
|
) {
|
||||||
|
NavigationBar(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
tonalElevation = 0.dp,
|
||||||
) {
|
) {
|
||||||
NavigationBar(containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface) {
|
|
||||||
destinations.forEach { destination ->
|
destinations.forEach { destination ->
|
||||||
val selected = backStack?.destination?.hierarchy?.any { it.route == destination.route } == true
|
val selected = backStack?.destination?.hierarchy?.any { it.route == destination.route } == true
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
@@ -72,7 +78,9 @@ fun AppRoot(
|
|||||||
icon = destination.icon,
|
icon = destination.icon,
|
||||||
label = { Text(destination.label) },
|
label = { Text(destination.label) },
|
||||||
colors = NavigationBarItemDefaults.colors(
|
colors = NavigationBarItemDefaults.colors(
|
||||||
indicatorColor = androidx.compose.material3.MaterialTheme.colorScheme.primaryContainer,
|
indicatorColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
selectedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
|
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -81,7 +89,7 @@ fun AppRoot(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
NavHost(navController = navController, startDestination = "tracker") {
|
NavHost(navController = navController, startDestination = "tracker", modifier = Modifier.fillMaxSize()) {
|
||||||
composable("tracker") { TrackerScreen(contentPadding = padding) }
|
composable("tracker") { TrackerScreen(contentPadding = padding) }
|
||||||
composable("settings") {
|
composable("settings") {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
@@ -102,7 +110,7 @@ private fun WhatsNewDialog(onDismiss: () -> Unit) {
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text("Neu in RipMe ${BuildConfig.VERSION_NAME}") },
|
title = { Text("Neu in RipMe ${BuildConfig.VERSION_NAME}") },
|
||||||
text = { Text("Modern Card Design, klarere Trainingseingaben und eine kompaktere Navigation.") },
|
text = { Text("Modern Card UI, sicherer Offline-Tracker und vollständige Analyseansichten.") },
|
||||||
confirmButton = { TextButton(onClick = onDismiss) { Text("Verstanden") } },
|
confirmButton = { TextButton(onClick = onDismiss) { Text("Verstanden") } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,64 +2,45 @@ package de.drazz.boehmitools.training.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Typography
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
val ModernPurple = Color(0xFF7C3AED)
|
|
||||||
val ModernViolet = Color(0xFF9B5CF6)
|
|
||||||
val ModernIndigo = Color(0xFF4F46E5)
|
|
||||||
val ModernGreen = Color(0xFF22C55E)
|
|
||||||
|
|
||||||
private val Light = lightColorScheme(
|
private val Light = lightColorScheme(
|
||||||
primary = ModernPurple,
|
primary = Color(0xFF7C3AED),
|
||||||
onPrimary = Color.White,
|
onPrimary = Color.White,
|
||||||
primaryContainer = Color(0xFFEDE9FE),
|
primaryContainer = Color(0xFFEDE9FE),
|
||||||
onPrimaryContainer = Color(0xFF32136B),
|
onPrimaryContainer = Color(0xFF2E1065),
|
||||||
secondary = ModernIndigo,
|
secondary = Color(0xFF4F46E5),
|
||||||
onSecondary = Color.White,
|
onSecondary = Color.White,
|
||||||
secondaryContainer = Color(0xFFE0E7FF),
|
secondaryContainer = Color(0xFFE0E7FF),
|
||||||
onSecondaryContainer = Color(0xFF1E1B4B),
|
tertiary = Color(0xFF06B6D4),
|
||||||
tertiary = ModernGreen,
|
background = Color(0xFFF4F5FB),
|
||||||
background = Color(0xFFF6F5FA),
|
onBackground = Color(0xFF171725),
|
||||||
onBackground = Color(0xFF17151D),
|
|
||||||
surface = Color(0xFFFFFFFF),
|
surface = Color(0xFFFFFFFF),
|
||||||
onSurface = Color(0xFF17151D),
|
onSurface = Color(0xFF171725),
|
||||||
surfaceVariant = Color(0xFFF0EEF5),
|
surfaceVariant = Color(0xFFF0F1F7),
|
||||||
onSurfaceVariant = Color(0xFF66616F),
|
onSurfaceVariant = Color(0xFF69697A),
|
||||||
outline = Color(0xFFDDD8E7),
|
outline = Color(0xFFD9DAE5),
|
||||||
outlineVariant = Color(0xFFEAE6F0),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val Dark = darkColorScheme(
|
private val Dark = darkColorScheme(
|
||||||
primary = Color(0xFFA78BFA),
|
primary = Color(0xFFA78BFA),
|
||||||
onPrimary = Color(0xFF221047),
|
onPrimary = Color(0xFF24005B),
|
||||||
primaryContainer = Color(0xFF4C1D95),
|
primaryContainer = Color(0xFF4C1D95),
|
||||||
onPrimaryContainer = Color(0xFFF3E8FF),
|
onPrimaryContainer = Color(0xFFEDE9FE),
|
||||||
secondary = Color(0xFF818CF8),
|
secondary = Color(0xFF818CF8),
|
||||||
onSecondary = Color(0xFF11133F),
|
onSecondary = Color(0xFF10144A),
|
||||||
secondaryContainer = Color(0xFF312E81),
|
secondaryContainer = Color(0xFF312E81),
|
||||||
onSecondaryContainer = Color(0xFFE0E7FF),
|
tertiary = Color(0xFF22D3EE),
|
||||||
tertiary = Color(0xFF34D399),
|
background = Color(0xFF0B0D17),
|
||||||
background = Color(0xFF0D111A),
|
onBackground = Color(0xFFF5F5FA),
|
||||||
onBackground = Color(0xFFF5F3FA),
|
surface = Color(0xFF151824),
|
||||||
surface = Color(0xFF151A25),
|
onSurface = Color(0xFFF5F5FA),
|
||||||
onSurface = Color(0xFFF5F3FA),
|
surfaceVariant = Color(0xFF202432),
|
||||||
surfaceVariant = Color(0xFF202634),
|
onSurfaceVariant = Color(0xFFB8BBC8),
|
||||||
onSurfaceVariant = Color(0xFFB7B2C3),
|
outline = Color(0xFF343849),
|
||||||
outline = Color(0xFF383F50),
|
|
||||||
outlineVariant = Color(0xFF282E3B),
|
|
||||||
)
|
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -69,9 +50,5 @@ fun TrainingTheme(theme: String, content: @Composable () -> Unit) {
|
|||||||
"light" -> false
|
"light" -> false
|
||||||
else -> isSystemInDarkTheme()
|
else -> isSystemInDarkTheme()
|
||||||
}
|
}
|
||||||
MaterialTheme(
|
MaterialTheme(colorScheme = if (dark) Dark else Light, content = content)
|
||||||
colorScheme = if (dark) Dark else Light,
|
|
||||||
typography = ModernTypography,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-144
@@ -11,15 +11,11 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.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.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
@@ -30,7 +26,6 @@ import androidx.compose.material.icons.filled.Stop
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -45,24 +40,21 @@ import androidx.compose.material3.OutlinedButton
|
|||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.PrimaryTabRow
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.drazz.boehmitools.training.ui.theme.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.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.drazz.boehmitools.training.data.local.PendingMutation
|
import de.drazz.boehmitools.training.data.local.PendingMutation
|
||||||
@@ -100,50 +92,71 @@ fun TrackerScreen(
|
|||||||
val pending by viewModel.pending.collectAsStateWithLifecycle()
|
val pending by viewModel.pending.collectAsStateWithLifecycle()
|
||||||
val currentPending = pending.filter { it.planId == state.selectedPlanId }
|
val currentPending = pending.filter { it.planId == state.selectedPlanId }
|
||||||
var planMenu by remember { mutableStateOf(false) }
|
var planMenu by remember { mutableStateOf(false) }
|
||||||
|
val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ")
|
||||||
|
|
||||||
Scaffold(
|
Column(
|
||||||
modifier = Modifier.padding(contentPadding),
|
Modifier
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
.fillMaxSize()
|
||||||
topBar = {
|
.padding(contentPadding)
|
||||||
TopAppBar(
|
.background(MaterialTheme.colorScheme.background),
|
||||||
title = {
|
) {
|
||||||
Column {
|
Row(
|
||||||
Text(state.plan?.title ?: "RipMe", maxLines = 2)
|
Modifier.fillMaxWidth().padding(start = 20.dp, end = 12.dp, top = 16.dp, bottom = 10.dp),
|
||||||
if (state.saveState.isNotBlank()) Text(state.saveState, style = MaterialTheme.typography.labelSmall)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text("TRAININGSPLAN", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Text(
|
||||||
|
state.plan?.title ?: "RipMe",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
)
|
||||||
|
if (state.saveState.isNotBlank()) {
|
||||||
|
Text(state.saveState, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = { planMenu = true }) {
|
IconButton(onClick = { planMenu = true }) { Icon(Icons.Default.MoreVert, contentDescription = "Plan wechseln") }
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = "Plan wechseln")
|
|
||||||
}
|
|
||||||
DropdownMenu(expanded = planMenu, onDismissRequest = { planMenu = false }) {
|
DropdownMenu(expanded = planMenu, onDismissRequest = { planMenu = false }) {
|
||||||
state.plans.forEach { plan ->
|
state.plans.forEach { plan ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(text = { Text(plan.title) }, onClick = { planMenu = false; viewModel.selectPlan(plan.id) })
|
||||||
text = { Text(plan.title) },
|
|
||||||
onClick = { planMenu = false; viewModel.selectPlan(plan.id) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
},
|
Row(
|
||||||
) { inner ->
|
Modifier
|
||||||
Column(Modifier.fillMaxSize().padding(inner)) {
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ")
|
.fillMaxWidth()
|
||||||
PrimaryTabRow(
|
.clip(RoundedCornerShape(18.dp))
|
||||||
selectedTabIndex = tabs.indexOfFirst { it.first == state.tab }.coerceAtLeast(0),
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
.padding(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
tabs.forEach { (id, label) ->
|
tabs.forEach { (id, label) ->
|
||||||
Tab(
|
val selected = state.tab == id
|
||||||
selected = state.tab == id,
|
Surface(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = if (selected) MaterialTheme.colorScheme.surface else Color.Transparent,
|
||||||
|
shadowElevation = if (selected) 3.dp else 0.dp,
|
||||||
onClick = { viewModel.setTab(id) },
|
onClick = { viewModel.setTab(id) },
|
||||||
text = { Text(label, maxLines = 2, style = MaterialTheme.typography.labelMedium) },
|
) {
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
modifier = Modifier.padding(vertical = 10.dp),
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
|
||||||
|
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
when {
|
when {
|
||||||
state.loading && state.plan == null -> Box(Modifier.fillMaxSize().padding(32.dp)) { CircularProgressIndicator() }
|
state.loading && state.plan == null -> Box(Modifier.fillMaxSize().padding(32.dp)) { CircularProgressIndicator() }
|
||||||
state.error.isNotBlank() && state.plan == null -> ErrorPane(state.error) { viewModel.loadPlans(force = true) }
|
state.error.isNotBlank() && state.plan == null -> ErrorPane(state.error) { viewModel.loadPlans(force = true) }
|
||||||
@@ -162,7 +175,7 @@ fun TrackerScreen(
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = viewModel::dismissConflict,
|
onDismissRequest = viewModel::dismissConflict,
|
||||||
title = { Text("Revisionskonflikt") },
|
title = { Text("Revisionskonflikt") },
|
||||||
text = { Text("Der Server steht bereits auf Revision ${conflict.currentRevision}. Deine lokale Änderung wird nicht automatisch überschrieben. Lade den Serverstand neu und wende die Änderung bewusst erneut an.") },
|
text = { Text("Der Server steht bereits auf Revision ${conflict.currentRevision}. Deine lokale Änderung wird nicht automatisch überschrieben.") },
|
||||||
confirmButton = { TextButton(onClick = viewModel::reloadAfterConflict) { Text("Serverstand neu laden") } },
|
confirmButton = { TextButton(onClick = viewModel::reloadAfterConflict) { Text("Serverstand neu laden") } },
|
||||||
dismissButton = { TextButton(onClick = viewModel::dismissConflict) { Text("Lokale Ansicht behalten") } },
|
dismissButton = { TextButton(onClick = viewModel::dismissConflict) { Text("Lokale Ansicht behalten") } },
|
||||||
)
|
)
|
||||||
@@ -183,33 +196,24 @@ private fun SessionPane(state: TrackerUiState, pending: List<PendingMutation>, v
|
|||||||
val day = plan.days.firstOrNull { it.num == state.day } ?: return
|
val day = plan.days.firstOrNull { it.num == state.day } ?: return
|
||||||
val session = trackerSession(state.tracker, de.drazz.boehmitools.training.domain.sessionKey(state.week, state.day), plan)
|
val session = trackerSession(state.tracker, de.drazz.boehmitools.training.domain.sessionKey(state.week, state.day), plan)
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 14.dp, vertical = 12.dp),
|
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
) {
|
) {
|
||||||
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error)
|
if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error)
|
||||||
if (pending.isNotEmpty()) PendingQueue(pending, vm)
|
if (pending.isNotEmpty()) PendingQueue(pending, vm)
|
||||||
WeekDayPicker(plan, state.tracker, state.week, state.day, vm)
|
|
||||||
SessionHeader(plan, day, session, state.week, vm)
|
SessionHeader(plan, day, session, state.week, vm)
|
||||||
|
WeekDayPicker(plan, state.tracker, state.week, state.day, vm)
|
||||||
|
|
||||||
SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm)
|
SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm)
|
||||||
day.rotations.forEach { rotation ->
|
day.rotations.forEach { rotation ->
|
||||||
Card(
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Text(rotation.label.uppercase(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
shape = RoundedCornerShape(22.dp),
|
rotation.exercises.forEach { exercise -> ExerciseCard(exercise, session, plan, vm) }
|
||||||
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 ->
|
|
||||||
ExerciseCard(exercise, session, plan, vm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SequenceCard("Cooldown", "cooldown", day.cooldown, session, vm)
|
SequenceCard("Cooldown", "cooldown", day.cooldown, session, vm)
|
||||||
SequenceCard("Stretch", "stretch", day.stretch, session, vm)
|
SequenceCard("Stretch", "stretch", day.stretch, session, vm)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,40 +292,26 @@ private fun WeekPicker(plan: PlanUi, tracker: JsonObject, week: Int, vm: Tracker
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: Int, vm: TrackerViewModel) {
|
private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: Int, vm: TrackerViewModel) {
|
||||||
val status = session.string("status", "planned")
|
val status = session.string("status", "planned")
|
||||||
val statusLabel = when (status) {
|
val statusLabel = when (status) { "in_progress" -> "Läuft"; "stopped" -> "Gestoppt"; "completed" -> "Abgeschlossen"; else -> "Geplant" }
|
||||||
"in_progress" -> "Läuft"
|
val gradient = Brush.linearGradient(listOf(Color(0xFF4F46E5), Color(0xFF9333EA)))
|
||||||
"stopped" -> "Gestoppt"
|
|
||||||
"completed" -> "Abgeschlossen"
|
|
||||||
else -> "Geplant"
|
|
||||||
}
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(24.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier.fillMaxWidth().clip(RoundedCornerShape(26.dp)).background(gradient).padding(20.dp),
|
||||||
.background(Brush.linearGradient(listOf(ModernPurple, ModernIndigo)))
|
|
||||||
.padding(20.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Text("Woche $week · Tag ${day.num}", color = Color.White.copy(alpha = .8f), style = MaterialTheme.typography.labelLarge)
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text(day.focus, color = Color.White, style = MaterialTheme.typography.headlineSmall)
|
Text("WOCHE $week · TAG ${day.num}", color = Color.White.copy(alpha = .78f), style = MaterialTheme.typography.labelMedium)
|
||||||
Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)} · $statusLabel", color = Color.White.copy(alpha = .78f), style = MaterialTheme.typography.bodySmall)
|
Text(statusLabel, color = Color.White, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
Text(day.focus, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = Color.White)
|
||||||
|
Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)}", color = Color.White.copy(alpha = .72f), style = MaterialTheme.typography.bodySmall)
|
||||||
when (status) {
|
when (status) {
|
||||||
"in_progress" -> Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
"in_progress" -> Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Button(onClick = { vm.setSessionStatus("completed") }, modifier = Modifier.weight(1f)) {
|
Button(onClick = { vm.setSessionStatus("completed") }, modifier = Modifier.weight(1f)) { Text("Abschließen") }
|
||||||
Icon(Icons.Default.CheckCircle, null); Spacer(Modifier.width(6.dp)); Text("Abschließen", maxLines = 1)
|
OutlinedButton(onClick = { vm.setSessionStatus("stopped") }, modifier = Modifier.weight(1f)) { Text("Stoppen", color = Color.White) }
|
||||||
}
|
}
|
||||||
OutlinedButton(onClick = { vm.setSessionStatus("stopped") }, modifier = Modifier.weight(1f)) {
|
"stopped" -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session fortsetzen") }
|
||||||
Icon(Icons.Default.Stop, null); Spacer(Modifier.width(6.dp)); Text("Stoppen", maxLines = 1)
|
"completed" -> OutlinedButton(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session wieder öffnen", color = Color.White) }
|
||||||
}
|
else -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session starten") }
|
||||||
}
|
|
||||||
"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)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,12 +319,8 @@ private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: I
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SequenceCard(title: String, prefix: String, items: List<String>, session: JsonObject, vm: TrackerViewModel) {
|
private fun SequenceCard(title: String, prefix: String, items: List<String>, session: JsonObject, vm: TrackerViewModel) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
Card(
|
Card(Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
|
||||||
) {
|
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
items.forEachIndexed { index, label ->
|
items.forEachIndexed { index, label ->
|
||||||
val id = "$prefix-$index"
|
val id = "$prefix-$index"
|
||||||
@@ -359,81 +345,53 @@ private fun ExerciseCard(exercise: ExerciseUi, session: JsonObject, plan: PlanUi
|
|||||||
var progressionMenu by remember(exercise.id, progression) { mutableStateOf(false) }
|
var progressionMenu by remember(exercise.id, progression) { mutableStateOf(false) }
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(22.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = .72f)),
|
colors = androidx.compose.material3.CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
elevation = androidx.compose.material3.CardDefaults.cardElevation(defaultElevation = 6.dp),
|
||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxWidth().padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(exercise.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
Text(exercise.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
if (exercise.cue.isNotBlank()) Text(exercise.cue, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
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))
|
Surface(
|
||||||
}
|
shape = RoundedCornerShape(50),
|
||||||
Text("ID ${exercise.id}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
color = if (status == "completed") Color(0xFF10B981) else MaterialTheme.colorScheme.surfaceVariant,
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
onClick = { vm.setItemStatus(exercise.id, if (status == "completed") "planned" else "completed") },
|
||||||
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) ->
|
Icon(Icons.Default.CheckCircle, null, modifier = Modifier.padding(8.dp), tint = if (status == "completed") Color.White else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
FilterChip(
|
|
||||||
selected = status == value,
|
|
||||||
onClick = { vm.setItemStatus(exercise.id, value) },
|
|
||||||
label = { Text(label, maxLines = 2, style = MaterialTheme.typography.labelSmall) },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
listOf("planned" to "Offen", "completed" to "Erledigt", "partial" to "Teilweise", "skipped" to "Skip").forEach { (value, label) ->
|
||||||
|
FilterChip(selected = status == value, onClick = { vm.setItemStatus(exercise.id, value) }, label = { Text(label, maxLines = 1) }, modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (status == "skipped") {
|
if (status == "skipped") {
|
||||||
OutlinedTextField(
|
OutlinedTextField(value = item.string("skip_reason"), onValueChange = { vm.setItemText(exercise.id, "skip_reason", it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Grund") }, shape = RoundedCornerShape(16.dp))
|
||||||
value = item.string("skip_reason"),
|
|
||||||
onValueChange = { vm.setItemText(exercise.id, "skip_reason", it) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
label = { Text("Grund") },
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = progression,
|
value = progression,
|
||||||
onValueChange = { vm.setItemProgression(exercise.id, it) },
|
onValueChange = { vm.setItemProgression(exercise.id, it) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
label = { Text("Progressionsstufe") },
|
label = { Text("Progressionsstufe") },
|
||||||
supportingText = { Text("Stufe überschreibt bei Bedarf das Ergebnisschema") },
|
shape = RoundedCornerShape(16.dp),
|
||||||
)
|
)
|
||||||
if (progressionModel?.steps?.isNotEmpty() == true) {
|
if (progressionModel?.steps?.isNotEmpty() == true) {
|
||||||
Box {
|
Box {
|
||||||
OutlinedButton(onClick = { progressionMenu = true }, modifier = Modifier.fillMaxWidth()) {
|
OutlinedButton(onClick = { progressionMenu = true }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp)) { Text("Stufe auswählen") }
|
||||||
Text("Stufe aus Plan auswählen")
|
|
||||||
}
|
|
||||||
DropdownMenu(expanded = progressionMenu, onDismissRequest = { progressionMenu = false }) {
|
DropdownMenu(expanded = progressionMenu, onDismissRequest = { progressionMenu = false }) {
|
||||||
progressionModel.steps.forEach { step ->
|
progressionModel.steps.forEach { step -> DropdownMenuItem(text = { Text(step.name) }, onClick = { progressionMenu = false; vm.setItemProgression(exercise.id, step.name, step.id) }) }
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(step.name) },
|
|
||||||
onClick = { progressionMenu = false; vm.setItemProgression(exercise.id, step.name, step.id) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
ManualResultFormatEditor(inherited = schema, manualOverride = item["result_format"] as? JsonObject, onChange = { vm.setItemResultFormat(exercise.id, it) })
|
||||||
}
|
|
||||||
ManualResultFormatEditor(
|
|
||||||
inherited = schema,
|
|
||||||
manualOverride = item["result_format"] as? JsonObject,
|
|
||||||
onChange = { vm.setItemResultFormat(exercise.id, it) },
|
|
||||||
)
|
|
||||||
ResultEditor(schema, data) { vm.setItemResultData(exercise.id, it) }
|
ResultEditor(schema, data) { vm.setItemResultData(exercise.id, it) }
|
||||||
OutlinedTextField(
|
OutlinedTextField(value = item.string("note"), onValueChange = { vm.setItemText(exercise.id, "note", it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Sessionnotiz") }, minLines = 2, shape = RoundedCornerShape(16.dp))
|
||||||
value = item.string("note"),
|
|
||||||
onValueChange = { vm.setItemText(exercise.id, "note", it) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
label = { Text("Sessionnotiz") },
|
|
||||||
minLines = 2,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user