Open on the car list so a car is chosen before its nights.

This commit is contained in:
RallyBlock committed 2026-10-22 21:15:46 -07:00
1 parent 13451a9955
commit 9a4db39b58
6 files changed
+175 -94

No files matched your search

@@ -8,6 +8,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.racingapp.pitlog.ui.guide.SetupGuideScreen
import com.racingapp.pitlog.ui.home.CarsScreen
import com.racingapp.pitlog.ui.home.HomeScreen
import com.racingapp.pitlog.ui.night.NightDetailScreen
import com.racingapp.pitlog.ui.night.NightEditorScreen
@@ -22,9 +23,22 @@ import com.racingapp.pitlog.ui.setup.CarSetupScreen
fun PitNav() {
val nav = rememberNavController()
val context = LocalContext.current
NavHost(navController = nav, startDestination = "home") {
NavHost(navController = nav, startDestination = "cars") {
composable("cars") {
CarsScreen(
onOpenCar = {
nav.navigate("home") {
popUpTo("cars")
launchSingleTop = true
}
},
onAddCar = { nav.navigate("settings") },
onSettings = { nav.navigate("settings") },
)
}
composable("home") {
HomeScreen(
onBack = { nav.popBackStack() },
onNewNight = { nav.navigate("editor/night") },
onOpenNight = { id -> nav.navigate("night/$id") },
onLogRun = { id -> nav.navigate("editor/run/$id") },
@@ -0,0 +1,145 @@
package com.racingapp.pitlog.ui.home
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import com.racingapp.pitlog.data.Car
import com.racingapp.pitlog.data.Repository
import com.racingapp.pitlog.ui.pitViewModel
import com.racingapp.pitlog.ui.theme.pitTopBarColors
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class CarsViewModel(private val repository: Repository) : ViewModel() {
val cars: StateFlow<List<Car>?> = repository.observeCars()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
fun open(id: Long, onOpen: () -> Unit) {
repository.selectCar(id)
onOpen()
}
fun addCar(onDone: () -> Unit) {
viewModelScope.launch {
repository.addCar()
onDone()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CarsScreen(
onOpenCar: () -> Unit,
onAddCar: () -> Unit,
onSettings: () -> Unit,
) {
val viewModel = pitViewModel { CarsViewModel(it) }
val cars by viewModel.cars.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Pit Log") },
colors = pitTopBarColors(),
actions = {
IconButton(onClick = onSettings) {
Icon(Icons.Filled.Settings, contentDescription = "Car")
}
IconButton(onClick = { viewModel.addCar(onAddCar) }) {
Icon(Icons.Filled.Add, contentDescription = "Add a car")
}
},
)
},
) { padding ->
if (cars == null) {
Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else if (cars!!.isEmpty()) {
Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
Column(
modifier = Modifier.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("No car yet", style = MaterialTheme.typography.titleLarge)
Text(
"Add a car to start a log. Each car gets its own 8-digit token.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp),
)
Button(
onClick = { viewModel.addCar(onAddCar) },
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
) {
Text("Add a car")
}
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = padding,
) {
item {
Text(
"Cars",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 8.dp),
)
}
items(cars.orEmpty(), key = { it.id }) { car ->
Card(
onClick = { viewModel.open(car.id, onOpenCar) },
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 6.dp)
.fillMaxWidth(),
) {
Column(
Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(car.carName, style = MaterialTheme.typography.titleLarge)
Text(
"${car.raceClass} · ${car.discipline}",
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
}
}
}
@@ -4,7 +4,6 @@ import androidx.compose.foundation.Canvas
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
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -13,6 +12,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Settings
@@ -20,8 +20,6 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -31,14 +29,11 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
@@ -49,7 +44,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import com.racingapp.pitlog.data.Car
import com.racingapp.pitlog.data.DEFAULT_CAR
import com.racingapp.pitlog.data.DEFAULT_CLASS
import com.racingapp.pitlog.data.DEFAULT_DISCIPLINE
import com.racingapp.pitlog.data.NightWithRuns
@@ -94,19 +88,12 @@ class HomeViewModel(private val repository: Repository) : ViewModel() {
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), HomeState())
fun selectCar(id: Long) = repository.selectCar(id)
fun addCar(onDone: () -> Unit) {
viewModelScope.launch {
repository.addCar()
onDone()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
onBack: () -> Unit,
onNewNight: () -> Unit,
onOpenNight: (Long) -> Unit,
onLogRun: (Long) -> Unit,
@@ -126,8 +113,13 @@ fun HomeScreen(
snackbarHost = { SnackbarHost(snackbar) },
topBar = {
TopAppBar(
title = { Text("Pit Log") },
title = { Text(state.profile?.carName?.ifBlank { "Pit Log" } ?: "Pit Log") },
colors = pitTopBarColors(),
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Cars")
}
},
actions = {
IconButton(onClick = onGuide) {
Icon(Icons.AutoMirrored.Filled.MenuBook, contentDescription = "Setup")
@@ -150,15 +142,8 @@ fun HomeScreen(
IconButton(onClick = onSettings) {
Icon(Icons.Filled.Settings, contentDescription = "Car")
}
IconButton(
onClick = {
if (state.cars.isEmpty()) viewModel.addCar(onSettings) else onNewNight()
},
) {
Icon(
Icons.Filled.Add,
contentDescription = if (state.cars.isEmpty()) "Add a car" else "New race night",
)
IconButton(onClick = onNewNight) {
Icon(Icons.Filled.Add, contentDescription = "New race night")
}
},
)
@@ -173,19 +158,13 @@ fun HomeScreen(
modifier = Modifier.fillMaxSize(),
contentPadding = padding,
) {
if (state.cars.isEmpty()) {
item { EmptyGarage(onAddCar = { viewModel.addCar(onSettings) }) }
} else {
item {
HomeHeader(
profile = state.profile,
cars = state.cars,
tonight = tonight,
onLogRun = onLogRun,
onNewNight = onNewNight,
onCarSetup = onCarSetup,
onSelectCar = viewModel::selectCar,
onAddCar = { viewModel.addCar(onSettings) },
)
}
if (state.nights.isEmpty()) {
@@ -203,7 +182,6 @@ fun HomeScreen(
NightCard(night, onClick = { onOpenNight(night.night.id) })
}
}
}
}
}
}
@@ -212,52 +190,17 @@ fun HomeScreen(
@Composable
private fun HomeHeader(
profile: Car?,
cars: List<Car>,
tonight: NightWithRuns?,
onLogRun: (Long) -> Unit,
onNewNight: () -> Unit,
onCarSetup: () -> Unit,
onSelectCar: (Long) -> Unit,
onAddCar: () -> Unit,
) {
val car = profile?.carName?.ifBlank { DEFAULT_CAR } ?: DEFAULT_CAR
val raceClass = profile?.raceClass?.ifBlank { DEFAULT_CLASS } ?: DEFAULT_CLASS
val discipline = profile?.discipline?.ifBlank { DEFAULT_DISCIPLINE } ?: DEFAULT_DISCIPLINE
var menu by remember { mutableStateOf(false) }
Column(Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
CarMark()
Column(Modifier.padding(start = 12.dp)) {
Box {
TextButton(
onClick = { menu = true },
contentPadding = PaddingValues(0.dp),
) {
Text(
car,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) {
cars.forEach { item ->
DropdownMenuItem(
text = { Text(item.carName) },
onClick = {
menu = false
onSelectCar(item.id)
},
)
}
DropdownMenuItem(
text = { Text("Add a car") },
onClick = {
menu = false
onAddCar()
},
)
}
}
Text(
"$raceClass · $discipline",
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -294,29 +237,6 @@ private fun HomeHeader(
}
}
@Composable
private fun EmptyGarage(onAddCar: () -> Unit) {
Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
Text(
"No car yet",
style = MaterialTheme.typography.titleLarge,
)
Text(
"Add a car to start a log. Each car gets its own 8-digit token.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp),
)
Button(
onClick = onAddCar,
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
) {
Text("Add a car")
}
}
}
@Composable
private fun EmptyNights() {
Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {