Start with no cars so the first one is added on purpose.

This commit is contained in:
RallyBlock committed 2026-10-22 21:11:56 -07:00
1 parent a164d9c135
commit 13451a9955
8 files changed
+89 -36

No files matched your search

+2
View File
@@ -6,6 +6,8 @@ Pressures are PSI. Tire heat is °F, read inside, middle, then outside.
## On the phone ## On the phone
The phone starts with no cars. Add one from the home screen, or paste a token from another phone.
The gear icon opens **Car**. A new install already has the server address filled in: The gear icon opens **Car**. A new install already has the server address filled in:
`http://sync.sleepingliongames.com:27017` `http://sync.sleepingliongames.com:27017`
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "com.racingapp.pitlog" applicationId = "com.racingapp.pitlog"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 2 versionCode = 3
versionName = "1.1" versionName = "1.2"
} }
buildTypes { buildTypes {
@@ -246,19 +246,6 @@ abstract class AppDatabase : RoomDatabase() {
fun create(context: Context): AppDatabase { fun create(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "pitlog.db") return Room.databaseBuilder(context, AppDatabase::class.java, "pitlog.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9) .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
.addCallback(
object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
db.execSQL(
"""
INSERT INTO cars (carName, raceClass, discipline, token, updatedAt, syncRev, deleted, pending)
VALUES ('$DEFAULT_CAR', '$DEFAULT_CLASS', '$DEFAULT_DISCIPLINE', printf('%08d', abs(random()) % 100000000), 1, 0, 0, 1)
""".trimIndent(),
)
}
},
)
.build() .build()
} }
} }
@@ -123,6 +123,12 @@ interface RaceDao {
@Query("SELECT * FROM cars ORDER BY id ASC") @Query("SELECT * FROM cars ORDER BY id ASC")
suspend fun everyCar(): List<Car> suspend fun everyCar(): List<Car>
@Query("SELECT COUNT(*) FROM race_nights WHERE carId = :carId AND deleted = 0")
suspend fun nightCount(carId: Long): Int
@Query("SELECT COUNT(*) FROM setup_sheets WHERE carId = :carId AND deleted = 0")
suspend fun sheetCount(carId: Long): Int
@Query("SELECT * FROM cars WHERE deleted = 0 ORDER BY id ASC") @Query("SELECT * FROM cars WHERE deleted = 0 ORDER BY id ASC")
suspend fun activeCars(): List<Car> suspend fun activeCars(): List<Car>
@@ -10,22 +10,18 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
suspend fun ensureCars(dao: RaceDao, preferences: SyncPreferences) { suspend fun ensureCars(dao: RaceDao, preferences: SyncPreferences) {
val cars = dao.everyCar() for (car in dao.everyCar()) {
if (cars.isEmpty()) { if (car.deleted) continue
val id = dao.insertCar( val untouched = car.updatedAt == 1L &&
Car( car.carName == DEFAULT_CAR &&
carName = DEFAULT_CAR, car.raceClass == DEFAULT_CLASS &&
raceClass = DEFAULT_CLASS, car.discipline == DEFAULT_DISCIPLINE &&
discipline = DEFAULT_DISCIPLINE, dao.nightCount(car.id) == 0 &&
token = newCarToken(), dao.sheetCount(car.id) == 0
updatedAt = 1, if (untouched) {
pending = true, dao.updateCar(car.copy(deleted = true, pending = false))
), continue
)
preferences.selectedCarId = id
return
} }
for (car in cars) {
if (isEightDigitToken(car.token)) continue if (isEightDigitToken(car.token)) continue
val source = car.token.ifBlank { preferences.token.trim() } val source = car.token.ifBlank { preferences.token.trim() }
val next = if (source.isBlank()) newCarToken() else eightDigitToken(source) val next = if (source.isBlank()) newCarToken() else eightDigitToken(source)
@@ -34,10 +30,14 @@ suspend fun ensureCars(dao: RaceDao, preferences: SyncPreferences) {
dao.markRunsPending(car.id) dao.markRunsPending(car.id)
dao.markSheetsPending(car.id) dao.markSheetsPending(car.id)
} }
val first = cars.firstOrNull { !it.deleted } ?: cars.first() val active = dao.activeCars()
if (active.isEmpty()) {
preferences.selectedCarId = 0
return
}
val selected = preferences.selectedCarId val selected = preferences.selectedCarId
if (selected == 0L || cars.none { it.id == selected && !it.deleted }) { if (selected == 0L || active.none { it.id == selected }) {
preferences.selectedCarId = first.id preferences.selectedCarId = active.first().id
} }
} }
@@ -150,8 +150,15 @@ fun HomeScreen(
IconButton(onClick = onSettings) { IconButton(onClick = onSettings) {
Icon(Icons.Filled.Settings, contentDescription = "Car") Icon(Icons.Filled.Settings, contentDescription = "Car")
} }
IconButton(onClick = onNewNight) { IconButton(
Icon(Icons.Filled.Add, contentDescription = "New race night") 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",
)
} }
}, },
) )
@@ -166,6 +173,9 @@ fun HomeScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = padding, contentPadding = padding,
) { ) {
if (state.cars.isEmpty()) {
item { EmptyGarage(onAddCar = { viewModel.addCar(onSettings) }) }
} else {
item { item {
HomeHeader( HomeHeader(
profile = state.profile, profile = state.profile,
@@ -196,6 +206,7 @@ fun HomeScreen(
} }
} }
} }
}
} }
@Composable @Composable
@@ -283,6 +294,29 @@ 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 @Composable
private fun EmptyNights() { private fun EmptyNights() {
Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
@@ -366,3 +400,4 @@ private fun CarMark() {
) )
} }
} }
@@ -200,7 +200,7 @@ fun SettingsScreen(onBack: () -> Unit) {
) )
}, },
bottomBar = { bottomBar = {
if (viewModel.ready) { if (viewModel.ready && viewModel.carId != 0L) {
PitActionButton( PitActionButton(
text = if (viewModel.saving) "Saving…" else "Save", text = if (viewModel.saving) "Saving…" else "Save",
onClick = { viewModel.save(onBack) }, onClick = { viewModel.save(onBack) },
@@ -215,6 +215,20 @@ fun SettingsScreen(onBack: () -> Unit) {
} }
} else { } else {
PitScroll(padding) { PitScroll(padding) {
if (viewModel.carId == 0L) {
Text(
"Add a car before you log a night. Pressures are PSI and tire heat is °F.",
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
OutlinedButton(
onClick = viewModel::addCar,
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
) {
Text("Add a car")
}
} else {
Text( Text(
"New race nights start with this car. Pressures are PSI and tire heat is °F.", "New race nights start with this car. Pressures are PSI and tire heat is °F.",
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -276,6 +290,7 @@ fun SettingsScreen(onBack: () -> Unit) {
) { ) {
Text("Add a car") Text("Add a car")
} }
}
SectionLabel("Add a car from a token") SectionLabel("Add a car from a token")
WordsField( WordsField(
label = "Token from another phone", label = "Token from another phone",
@@ -33,6 +33,14 @@ class PreviewApp : RacingApp() {
super.onCreate() super.onCreate()
repository.saveAddress("") repository.saveAddress("")
runBlocking(Dispatchers.IO) { runBlocking(Dispatchers.IO) {
val car = repository.addCar()
repository.saveCar(
car.copy(
carName = DEFAULT_CAR,
raceClass = DEFAULT_CLASS,
discipline = DEFAULT_DISCIPLINE,
),
)
val nightId = repository.saveNight( val nightId = repository.saveNight(
RaceNight( RaceNight(
dateEpochDay = LocalDate.now().toEpochDay(), dateEpochDay = LocalDate.now().toEpochDay(),