diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b86273d..b589d56 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index f7608ed..adb8ae0 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/trackpro/MainActivity.kt b/app/src/main/java/com/example/trackpro/MainActivity.kt
index 89d0689..e21af90 100644
--- a/app/src/main/java/com/example/trackpro/MainActivity.kt
+++ b/app/src/main/java/com/example/trackpro/MainActivity.kt
@@ -10,7 +10,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -96,8 +95,11 @@ import com.example.trackpro.viewModels.VehicleFULLViewModel
import com.example.trackpro.viewModels.VehicleFULLViewModelFactory
import com.example.trackpro.viewModels.VehicleViewModel
import com.example.trackpro.viewModels.VehicleViewModelFactory
+import com.example.trackpro.components.pressableRow
+import com.example.trackpro.components.pressable
import com.example.trackpro.components.AppCard
import com.example.trackpro.components.SectionLabel
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
import com.example.trackpro.theme.TrackProType
@@ -317,17 +319,20 @@ class MainActivity : ComponentActivity() {
)
}
composable("drag") {
- DragRaceScreen(database, sessionManager, vehicleFULLViewModel)
+ DragRaceScreen(database, sessionManager, vehicleFULLViewModel, onBack = { navController.popBackStack() })
}
composable("esptest") {
- ESPConnectionTestScreen(onNavigateToSettings = { navController.navigate("settings") })
+ ESPConnectionTestScreen(
+ onNavigateToSettings = { navController.navigate("settings") },
+ onBack = { navController.popBackStack() }
+ )
}
composable(
"track/{trackId}",
arguments = listOf(navArgument("trackId") { type = NavType.LongType })
) { backStackEntry ->
val trackId = backStackEntry.arguments?.getLong("trackId") ?: 0L
- TrackScreen(trackId = trackId)
+ TrackScreen(trackId = trackId, onBack = { navController.popBackStack() })
}
composable("dragsessions") {
DragTimesListView(viewModel = dragSessionViewModel, navController = navController)
@@ -353,7 +358,7 @@ class MainActivity : ComponentActivity() {
arguments = listOf(navArgument("vehicleid") { type = NavType.LongType })
) { backStackEntry ->
val vehicleId = backStackEntry.arguments?.getLong("vehicleid") ?: 0L
- CarViewScreen(vehicleId = vehicleId)
+ CarViewScreen(vehicleId = vehicleId, onBack = { navController.popBackStack() })
}
composable(
route = "timeattacklistitem/{sessionid}",
@@ -367,12 +372,12 @@ class MainActivity : ComponentActivity() {
)
}
composable(route = "createvehicle") {
- CarCreationScreen(database)
+ CarCreationScreen(database, onBack = { navController.popBackStack() })
}
composable(route = "timeattack/{vehicleId}/{trackId}") { backStackEntry ->
val vehicleId = backStackEntry.arguments?.getString("vehicleId")?.toLongOrNull() ?: -1L
val trackId = backStackEntry.arguments?.getString("trackId")?.toLongOrNull() ?: -1L
- TimeAttackScreenView(vehicleId = vehicleId, trackId = trackId)
+ TimeAttackScreenView(vehicleId = vehicleId, trackId = trackId, onBack = { navController.popBackStack() })
}
composable(route = "trackandvehicle") {
TrackVehicleSelectorScreen(trackViewModel = trackViewModel, vehicleViewModel, navController)
@@ -451,7 +456,7 @@ fun MainScreen(
)
Text(
text = "Performance Telemetry",
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = TrackProTheme.colors.textFaint
)
}
@@ -544,7 +549,7 @@ fun MainScreen(
}
Text(
text = "TRACKPRO",
- style = TrackProType.label.copy(fontSize = 13.sp, letterSpacing = 2.sp),
+ style = TrackProType.label.atSize(13.sp).copy(letterSpacing = 2.sp),
color = TrackProTheme.colors.textPrimary
)
// Spacer to balance the row
@@ -705,14 +710,14 @@ private fun ActionCard(
val alpha = if (disabled) 0.4f else 1f
val iconSize = if (halfWidth) 14.dp else if (fullWidth) 18.dp else 16.dp
val iconBoxSize = if (halfWidth) 26.dp else if (fullWidth) 34.dp else 30.dp
- val titleStyle = if (fullWidth) TrackProType.titleMedium else TrackProType.titleMedium.copy(fontSize = 13.sp)
+ val titleStyle = if (fullWidth) TrackProType.titleMedium else TrackProType.titleMedium.atSize(13.sp)
val subtitleSize = if (halfWidth) 9.sp else 10.sp
val vertPadding = if (halfWidth) 10.dp else if (fullWidth) 12.dp else 10.dp
AppCard(
modifier = Modifier
.fillMaxWidth()
- .then(if (!disabled) Modifier.clickable(onClick = onClick) else Modifier),
+ .pressable(onClick = onClick, enabled = !disabled),
padding = 0.dp
) {
Box {
@@ -758,7 +763,7 @@ private fun ActionCard(
)
Text(
text = subtitle,
- style = TrackProType.body.copy(fontSize = subtitleSize),
+ style = TrackProType.body.atSize(subtitleSize),
color = TrackProTheme.colors.textMuted.copy(alpha = alpha),
maxLines = 2,
overflow = TextOverflow.Ellipsis
@@ -799,7 +804,7 @@ private fun DrawerItem(
Row(
modifier = Modifier
.fillMaxWidth()
- .clickable(onClick = onClick)
+ .pressableRow(onClick = onClick)
.padding(horizontal = 20.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
diff --git a/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt b/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt
index cae53e9..a1a8d31 100644
--- a/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt
@@ -2,12 +2,9 @@ package com.example.trackpro.screens
import android.annotation.SuppressLint
import android.graphics.Typeface
-import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -40,12 +37,15 @@ import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
import com.example.trackpro.TrackProApp
import com.example.trackpro.extrasForUI.TrackProTheme
+import com.example.trackpro.components.pressable
import com.example.trackpro.components.AppTopBar
import com.example.trackpro.components.SectionLabel
import com.example.trackpro.components.StatCell
import com.example.trackpro.components.StatCellDivider
import com.example.trackpro.components.StatCellSize
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.DataVizColors
+import com.example.trackpro.theme.Motion
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProType
import com.example.trackpro.managerClasses.JsonReader
@@ -56,7 +56,10 @@ import kotlin.math.sin
@SuppressLint("MissingPermission")
@Composable
-fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) {
+fun ESPConnectionTestScreen(
+ onNavigateToSettings: () -> Unit,
+ onBack: () -> Unit
+) {
val context = LocalContext.current
val app = context.applicationContext as TrackProApp
@@ -104,12 +107,17 @@ fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) {
GpsProviderType.PHONE_GPS -> "Phone GPS Mode"
},
accent = if (isConnected) TrackProTheme.colors.accent else TrackProTheme.colors.textFaint,
+ onBack = onBack,
trailing = {
Text(
text = "Change",
style = TrackProType.label,
color = TrackProTheme.colors.accent,
- modifier = Modifier.clickable { onNavigateToSettings() }
+ // Bare text was the smallest tap target on the screen; give it
+ // real padding and a press response.
+ modifier = Modifier
+ .pressable(onClick = onNavigateToSettings, scale = 0.94f)
+ .padding(horizontal = Spacing.sm, vertical = 4.dp)
)
}
)
@@ -250,7 +258,10 @@ fun StyledSpeedometer(
) {
val animatedSpeed by animateFloatAsState(
targetValue = speed,
- animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing),
+ // Was tween(600): every GPS tick restarted a fixed ramp from wherever it had
+ // got to, so a steadily changing speed animated in visible steps. A spring
+ // re-targets continuously from the current value instead.
+ animationSpec = Motion.standard(),
label = "speed"
)
@@ -396,7 +407,7 @@ private fun TelemetryRow(label: String, value: String, textPrimary: Color, textM
Text(label.uppercase(), style = TrackProType.label, color = textMuted)
Text(
value,
- style = TrackProType.body.copy(fontSize = 13.sp, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace),
+ style = TrackProType.body.atSize(13.sp).copy(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace),
color = textPrimary
)
}
diff --git a/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt b/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt
index 13c388b..5b3ccb4 100644
--- a/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt
@@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -27,12 +25,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.trackpro.TrackProApp
import com.example.trackpro.components.AppCard
-import com.example.trackpro.components.AppTopBar
+import com.example.trackpro.components.ScreenScaffold
+import com.example.trackpro.components.isScrolledUnderChrome
import com.example.trackpro.components.SectionLabel
import com.example.trackpro.components.ToggleChip
import com.example.trackpro.extrasForUI.AppDropdownField
import com.example.trackpro.extrasForUI.TrackProTheme
import com.example.trackpro.models.GpsProviderType
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProType
@@ -49,17 +49,24 @@ fun SettingsScreen(onBack: () -> Unit, onRequestBluetoothPermission: () -> Unit)
val useDarkTheme by app.useDarkTheme.collectAsState()
val useMetric by app.useMetricUnits.collectAsState()
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(TrackProTheme.colors.bgDeep)
- ) {
- AppTopBar(title = "Settings", accent = TrackProTheme.colors.textMuted, onBack = onBack)
+ val scrollState = rememberScrollState()
+ val scrolled by scrollState.isScrolledUnderChrome()
+ ScreenScaffold(
+ title = "Settings",
+ accent = TrackProTheme.colors.textMuted,
+ onBack = onBack,
+ contentScrolled = scrolled
+ ) { contentPadding ->
Column(
modifier = Modifier
- .padding(Spacing.md)
- .verticalScroll(rememberScrollState()),
+ .verticalScroll(scrollState)
+ .padding(
+ top = contentPadding.calculateTopPadding() + Spacing.md,
+ start = Spacing.md,
+ end = Spacing.md,
+ bottom = Spacing.md
+ ),
verticalArrangement = Arrangement.spacedBy(Spacing.md)
) {
// --- Section: Hardware & GPS ---
@@ -252,7 +259,7 @@ private fun GpsRateRow(selectedHz: Int, confirmedHz: Int?, onSelect: (Int) -> Un
if (confirmedHz != null) {
Text(
text = if (confirmedHz == selectedHz) "· confirmed" else "· device at ${confirmedHz}Hz",
- style = TrackProType.body.copy(fontSize = 10.sp),
+ style = TrackProType.body.atSize(10.sp),
// A mismatch between requested and confirmed rate is a real problem
// worth flagging, so this is one of the few places color is earned.
color = if (confirmedHz == selectedHz) TrackProTheme.colors.deltaGood
diff --git a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt
index ae185a4..a0df0fa 100644
--- a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt
@@ -46,10 +46,12 @@ import com.example.trackpro.dataClasses.TrackCoordinatesData
import com.example.trackpro.dataClasses.TrackMainData
import com.example.trackpro.dataClasses.LatLonOffset
import com.example.trackpro.extrasForUI.TrackProTheme
+import com.example.trackpro.components.Haptic
import com.example.trackpro.components.AppCard
import com.example.trackpro.components.AppTopBar
import com.example.trackpro.components.PrimaryButton
import com.example.trackpro.components.ToggleChip
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.DataVizColors
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
@@ -301,7 +303,7 @@ private fun TrackInfoCard(name: String, country: String, mode: String, onClick:
style = TrackProType.titleMedium,
color = TrackProTheme.colors.textPrimary
)
- Text("Mode: ${mode.uppercase()}", style = TrackProType.body.copy(fontSize = 12.sp), color = TrackProTheme.colors.accent)
+ Text("Mode: ${mode.uppercase()}", style = TrackProType.body.atSize(12.sp), color = TrackProTheme.colors.accent)
}
PrimaryButton(
text = "Edit",
@@ -320,6 +322,7 @@ private fun MarkSectorButton(count: Int, enabled: Boolean, onClick: () -> Unit)
text = "Mark Sector ${count + 1}",
onClick = onClick,
enabled = enabled,
+ haptic = Haptic.Confirm,
accent = TrackProTheme.colors.accent,
modifier = Modifier.fillMaxWidth().height(48.dp)
)
@@ -395,6 +398,7 @@ private fun LiveControls(isRecording: Boolean, onToggle: () -> Unit) {
PrimaryButton(
text = if (isRecording) "Stop Recording" else "Start GPS Recording",
onClick = onToggle,
+ haptic = Haptic.Confirm,
accent = if (isRecording) TrackProTheme.colors.danger.copy(alpha = 0.18f) else TrackProTheme.colors.bgElevated,
contentColor = if (isRecording) TrackProTheme.colors.danger else TrackProTheme.colors.textPrimary,
modifier = Modifier.fillMaxWidth().height(56.dp)
@@ -415,6 +419,7 @@ private fun ManualControls(onUndo: () -> Unit, onSave: () -> Unit, canSave: Bool
text = "Save Track",
onClick = onSave,
enabled = canSave,
+ haptic = Haptic.Confirm,
accent = TrackProTheme.colors.accent,
modifier = Modifier.weight(1f).height(56.dp)
)
diff --git a/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt b/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt
index eba2eb8..6bbc7e0 100644
--- a/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt
@@ -1,7 +1,6 @@
package com.example.trackpro.screens
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -37,10 +36,13 @@ import com.example.trackpro.TrackProApp
import com.example.trackpro.dataClasses.TrackCoordinatesData
import com.example.trackpro.dataClasses.TrackMainData
import com.example.trackpro.extrasForUI.TrackProTheme
+import com.example.trackpro.components.Haptic
+import com.example.trackpro.components.pressable
import com.example.trackpro.components.AppTopBar
import com.example.trackpro.components.SectionLabel
import com.example.trackpro.components.StatCell
import com.example.trackpro.components.ToggleChip
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.DataVizColors
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProType
@@ -62,15 +64,15 @@ import org.maplibre.android.style.sources.GeoJsonSource
@Composable
-fun TrackScreen(trackId: Long) {
+fun TrackScreen(trackId: Long, onBack: () -> Unit) {
val context = LocalContext.current
val app = context.applicationContext as TrackProApp
val database = app.database
- TrackView(database, trackId)
+ TrackView(database, trackId, onBack)
}
@Composable
-fun TrackView(database: ESPDatabase, trackId: Long) {
+fun TrackView(database: ESPDatabase, trackId: Long, onBack: () -> Unit) {
val app = LocalContext.current.applicationContext as TrackProApp
val useMetric by app.useMetricUnits.collectAsState()
val coroutineScope = rememberCoroutineScope()
@@ -105,7 +107,11 @@ fun TrackView(database: ESPDatabase, trackId: Long) {
) {
Column(modifier = Modifier.fillMaxSize()) {
- AppTopBar(title = "Track Overview", accent = TrackProTheme.colors.accent)
+ AppTopBar(
+ title = "Track Overview",
+ accent = TrackProTheme.colors.accent,
+ onBack = onBack
+ )
// ── Track info card ───────────────────────────────
Column(
@@ -233,7 +239,7 @@ private fun SectorSlicerCard(
SectionLabel("Sectors")
Text(
text = if (sectorCount > 0) "$sectorCount marked" else "None marked",
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = if (sectorCount > 0) TrackProTheme.colors.accent else TrackProTheme.colors.textMuted
)
}
@@ -258,7 +264,9 @@ private fun SectorSlicerCard(
text = "Clear sectors",
style = TrackProType.label,
color = TrackProTheme.colors.danger,
- modifier = Modifier.clickable { onClear() }
+ modifier = Modifier
+ .pressable(onClick = onClear, scale = 0.94f, haptic = Haptic.Reject)
+ .padding(vertical = 4.dp)
)
}
}
diff --git a/app/src/main/java/com/example/trackpro/screens/TrackVehicleSelectorScreen.kt b/app/src/main/java/com/example/trackpro/screens/TrackVehicleSelectorScreen.kt
index 17f41ee..6c92d48 100644
--- a/app/src/main/java/com/example/trackpro/screens/TrackVehicleSelectorScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/TrackVehicleSelectorScreen.kt
@@ -34,6 +34,7 @@ import com.example.trackpro.extrasForUI.TrackProTheme
import com.example.trackpro.components.AppCard
import com.example.trackpro.components.AppTopBar
import com.example.trackpro.components.PrimaryButton
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProType
import com.example.trackpro.viewModels.TrackViewModel
@@ -111,7 +112,7 @@ fun TrackVehicleSelectorScreen(
selectedVehicleName = vehicles.find { it.vehicleId == id }?.manufacturerAndModel ?: "" // Adjust 'name' to your vehicle field
}
} else {
- Text("No vehicles found in garage", style = TrackProType.body.copy(fontSize = 12.sp), color = TrackProTheme.colors.accent)
+ Text("No vehicles found in garage", style = TrackProType.body.atSize(12.sp), color = TrackProTheme.colors.accent)
}
}
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/CarListView.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/CarListView.kt
index 67eb35b..5f9fcbb 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/CarListView.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/CarListView.kt
@@ -3,7 +3,6 @@ package com.example.trackpro.screens.listViewScreens
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -14,10 +13,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -40,10 +41,15 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.example.trackpro.dataClasses.VehicleInformationData
import com.example.trackpro.extrasForUI.TrackProTheme
-import com.example.trackpro.components.AppTopBar
+import com.example.trackpro.components.isScrolledUnderChrome
+import com.example.trackpro.components.ScreenScaffold
+import com.example.trackpro.components.Haptic
+import com.example.trackpro.components.pressable
+import com.example.trackpro.components.rememberHaptics
import com.example.trackpro.components.EmptyState
import com.example.trackpro.components.StatCell
import com.example.trackpro.components.StatCellDivider
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
import com.example.trackpro.theme.TrackProType
@@ -60,55 +66,57 @@ fun CarListScreen(navController: NavController, viewModel: VehicleFULLViewModel)
val scope = rememberCoroutineScope()
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(TrackProTheme.colors.bgDeep)
- ) {
- Column(modifier = Modifier.fillMaxSize()) {
+ val listState = rememberLazyListState()
+ val scrolled by listState.isScrolledUnderChrome()
- AppTopBar(
- title = "My Vehicles",
- accent = TrackProTheme.colors.accent,
- trailing = {
- Text(
- text = "${vehicles.size} cars",
- style = TrackProType.label,
- color = TrackProTheme.colors.textMuted
- )
- }
- )
-
- if (vehicles.isEmpty()) {
- EmptyState(
- message = "No vehicles yet",
- hint = "Add a vehicle from the main screen"
+ ScreenScaffold(
+ title = "My Vehicles",
+ onBack = { navController.popBackStack() },
+ accent = TrackProTheme.colors.accent,
+ trailing = {
+ Text(
+ text = "${vehicles.size} cars",
+ style = TrackProType.label,
+ color = TrackProTheme.colors.textMuted
)
- } else {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(Spacing.md),
- verticalArrangement = Arrangement.spacedBy(Spacing.sm)
- ) {
- items(vehicles) { vehicle ->
- VehicleCard(
- vehicle = vehicle,
- navController = navController,
- bgCard = TrackProTheme.colors.bgCard,
- bgElevated = TrackProTheme.colors.bgElevated,
- accent = TrackProTheme.colors.accent,
- dangerColor = TrackProTheme.colors.danger,
- textPrimary = TrackProTheme.colors.textPrimary,
- textMuted = TrackProTheme.colors.textMuted,
- sectorLine = TrackProTheme.colors.sectorLine,
- onDelete = { vehicleToDelete ->
- scope.launch(Dispatchers.IO) {
- database.vehicleInformationDAO()
- .deleteVehicle(vehicleToDelete.vehicleId)
- }
+ },
+ contentScrolled = scrolled
+ ) { contentPadding ->
+ if (vehicles.isEmpty()) {
+ EmptyState(
+ message = "No vehicles yet",
+ hint = "Add a vehicle from the main screen"
+ )
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = listState,
+ contentPadding = PaddingValues(
+ top = contentPadding.calculateTopPadding() + Spacing.md,
+ bottom = Spacing.md,
+ start = Spacing.md,
+ end = Spacing.md
+ ),
+ verticalArrangement = Arrangement.spacedBy(Spacing.sm)
+ ) {
+ items(vehicles) { vehicle ->
+ VehicleCard(
+ vehicle = vehicle,
+ navController = navController,
+ bgCard = TrackProTheme.colors.bgCard,
+ bgElevated = TrackProTheme.colors.bgElevated,
+ accent = TrackProTheme.colors.accent,
+ dangerColor = TrackProTheme.colors.danger,
+ textPrimary = TrackProTheme.colors.textPrimary,
+ textMuted = TrackProTheme.colors.textMuted,
+ sectorLine = TrackProTheme.colors.sectorLine,
+ onDelete = { vehicleToDelete ->
+ scope.launch(Dispatchers.IO) {
+ database.vehicleInformationDAO()
+ .deleteVehicle(vehicleToDelete.vehicleId)
}
- )
- }
+ }
+ )
}
}
}
@@ -129,6 +137,7 @@ fun VehicleCard(
onDelete: (VehicleInformationData) -> Unit
) {
var showDeleteDialog by remember { mutableStateOf(false) }
+ val haptics = rememberHaptics()
if (showDeleteDialog) {
AlertDialog(
@@ -138,6 +147,7 @@ fun VehicleCard(
textContentColor = TrackProTheme.colors.textMuted,
confirmButton = {
TextButton(onClick = {
+ haptics.perform(Haptic.Reject)
onDelete(vehicle)
showDeleteDialog = false
}) {
@@ -157,9 +167,9 @@ fun VehicleCard(
Box(
modifier = Modifier
.fillMaxWidth()
+ .pressable(onClick = { navController.navigate("vehicle/${vehicle.vehicleId}") })
.background(bgCard, TrackProShapes.card)
.border(1.dp, sectorLine, TrackProShapes.card)
- .clickable { navController.navigate("vehicle/${vehicle.vehicleId}") }
) {
Column {
@@ -189,13 +199,13 @@ fun VehicleCard(
) {
Text(
text = vehicle.fuelType.uppercase(),
- style = TrackProType.label.copy(fontSize = 9.sp),
+ style = TrackProType.label.atSize(9.sp),
color = textMuted
)
}
Text(
text = vehicle.drivetrain.uppercase(),
- style = TrackProType.label.copy(fontSize = 9.sp),
+ style = TrackProType.label.atSize(9.sp),
color = textMuted
)
}
@@ -203,9 +213,10 @@ fun VehicleCard(
// Delete button
Box(
modifier = Modifier
+ .minimumInteractiveComponentSize()
+ .pressable(onClick = { showDeleteDialog = true }, scale = 0.90f)
.size(26.dp)
- .background(dangerColor.copy(alpha = 0.1f), TrackProShapes.badge)
- .clickable { showDeleteDialog = true },
+ .background(dangerColor.copy(alpha = 0.1f), TrackProShapes.badge),
contentAlignment = Alignment.Center
) {
Icon(
@@ -232,7 +243,7 @@ fun VehicleCard(
)
Text(
text = "${vehicle.year} · ${vehicle.engineType}",
- style = TrackProType.body.copy(fontSize = 12.sp),
+ style = TrackProType.body.atSize(12.sp),
color = textMuted
)
}
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt
index 3351027..edc3eb5 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt
@@ -2,7 +2,6 @@ package com.example.trackpro.screens.listViewScreens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -15,6 +14,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
@@ -31,9 +31,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.example.trackpro.extrasForUI.TrackProTheme
-import com.example.trackpro.components.AppTopBar
+import com.example.trackpro.components.isScrolledUnderChrome
+import com.example.trackpro.components.ScreenScaffold
+import com.example.trackpro.components.pressable
import com.example.trackpro.components.EmptyState
import com.example.trackpro.components.ExpandableGroup
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
import com.example.trackpro.theme.TrackProType
@@ -58,40 +61,43 @@ fun DragTimesListView(
}
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(TrackProTheme.colors.bgDeep)
- ) {
- Column(modifier = Modifier.fillMaxSize()) {
- AppTopBar(
- title = "Drag Records",
- accent = TrackProTheme.colors.accent,
- trailing = {
- Text(
- "${dragSessions.size} sessions",
- style = TrackProType.label,
- color = TrackProTheme.colors.textMuted
- )
- }
- )
+ val listState = rememberLazyListState()
+ val scrolled by listState.isScrolledUnderChrome()
- if (dragSessions.isEmpty()) {
- EmptyState(message = "No sessions recorded", hint = "Run a drag session to see it here")
- } else {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(Spacing.md),
- verticalArrangement = Arrangement.spacedBy(Spacing.sm)
- ) {
- groupedSessions.forEach { (groupKey, sessions) ->
- item(key = groupKey) {
- ExpandableSessionGroup(
- groupTitle = groupKey,
- sessions = sessions,
- navController = navController
- )
- }
+ ScreenScaffold(
+ title = "Drag Records",
+ onBack = { navController.popBackStack() },
+ accent = TrackProTheme.colors.accent,
+ trailing = {
+ Text(
+ "${dragSessions.size} sessions",
+ style = TrackProType.label,
+ color = TrackProTheme.colors.textMuted
+ )
+ },
+ contentScrolled = scrolled
+ ) { contentPadding ->
+ if (dragSessions.isEmpty()) {
+ EmptyState(message = "No sessions recorded", hint = "Run a drag session to see it here")
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = listState,
+ contentPadding = PaddingValues(
+ top = contentPadding.calculateTopPadding() + Spacing.md,
+ bottom = Spacing.md,
+ start = Spacing.md,
+ end = Spacing.md
+ ),
+ verticalArrangement = Arrangement.spacedBy(Spacing.sm)
+ ) {
+ groupedSessions.forEach { (groupKey, sessions) ->
+ item(key = groupKey) {
+ ExpandableSessionGroup(
+ groupTitle = groupKey,
+ sessions = sessions,
+ navController = navController
+ )
}
}
}
@@ -108,10 +114,10 @@ fun ExpandableSessionGroup(
accent = TrackProTheme.colors.accent,
header = {
Column(modifier = Modifier.weight(1f)) {
- Text(groupTitle, style = TrackProType.titleMedium.copy(fontSize = 13.sp), color = TrackProTheme.colors.textPrimary)
+ Text(groupTitle, style = TrackProType.titleMedium.atSize(13.sp), color = TrackProTheme.colors.textPrimary)
Text(
"${sessions.size} runs completed",
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = TrackProTheme.colors.textMuted
)
}
@@ -124,8 +130,8 @@ fun ExpandableSessionGroup(
Row(
modifier = Modifier
.fillMaxWidth()
+ .pressable(onClick = { navController.navigate("graph/${session.sessionId}") })
.background(TrackProTheme.colors.bgElevated, TrackProShapes.control)
- .clickable { navController.navigate("graph/${session.sessionId}") }
.padding(Spacing.sm),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/TimeAttackListView.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/TimeAttackListView.kt
index 4739d16..c446309 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/TimeAttackListView.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/TimeAttackListView.kt
@@ -2,7 +2,6 @@ package com.example.trackpro.screens.listViewScreens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -15,6 +14,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -31,9 +31,12 @@ import androidx.navigation.NavController
import com.example.trackpro.dataClasses.SessionData
import com.example.trackpro.dataClasses.VehicleInformationData
import com.example.trackpro.extrasForUI.TrackProTheme
-import com.example.trackpro.components.AppTopBar
+import com.example.trackpro.components.isScrolledUnderChrome
+import com.example.trackpro.components.ScreenScaffold
+import com.example.trackpro.components.pressable
import com.example.trackpro.components.EmptyState
import com.example.trackpro.components.ExpandableGroup
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProType
import com.example.trackpro.viewModels.SessionViewModel
@@ -62,35 +65,42 @@ fun TimeAttackListViewScreen(
}
- Box(modifier = Modifier.fillMaxSize().background(TrackProTheme.colors.bgDeep)) {
- Column(modifier = Modifier.fillMaxSize()) {
- AppTopBar(
- title = "Track Records",
- accent = TrackProTheme.colors.accent,
- trailing = {
- Text("${trackSessions.size} sessions", style = TrackProType.label, color = TrackProTheme.colors.textMuted)
- }
- )
+ val listState = rememberLazyListState()
+ val scrolled by listState.isScrolledUnderChrome()
- if (trackSessions.isEmpty()) {
- EmptyState(message = "No sessions recorded", hint = "Run a track session to see it here")
- } else {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(Spacing.md),
- verticalArrangement = Arrangement.spacedBy(Spacing.sm)
- ) {
- groupedByTrack.forEach { (trackName, sessions) ->
- item(key = trackName) {
- val track = tracks.find { it.trackName == trackName }
- ExpandableTrackGroup(
- trackName = trackName,
- trackMeta = "${track?.country} · ${track?.type}",
- sessions = sessions,
- vehicles = vehicles,
- navController = navController
- )
- }
+ ScreenScaffold(
+ title = "Track Records",
+ onBack = { navController.popBackStack() },
+ accent = TrackProTheme.colors.accent,
+ trailing = {
+ Text("${trackSessions.size} sessions", style = TrackProType.label, color = TrackProTheme.colors.textMuted)
+ },
+ contentScrolled = scrolled
+ ) { contentPadding ->
+ if (trackSessions.isEmpty()) {
+ EmptyState(message = "No sessions recorded", hint = "Run a track session to see it here")
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = listState,
+ contentPadding = PaddingValues(
+ top = contentPadding.calculateTopPadding() + Spacing.md,
+ bottom = Spacing.md,
+ start = Spacing.md,
+ end = Spacing.md
+ ),
+ verticalArrangement = Arrangement.spacedBy(Spacing.sm)
+ ) {
+ groupedByTrack.forEach { (trackName, sessions) ->
+ item(key = trackName) {
+ val track = tracks.find { it.trackName == trackName }
+ ExpandableTrackGroup(
+ trackName = trackName,
+ trackMeta = "${track?.country} · ${track?.type}",
+ sessions = sessions,
+ vehicles = vehicles,
+ navController = navController
+ )
}
}
}
@@ -110,10 +120,10 @@ fun ExpandableTrackGroup(
accent = TrackProTheme.colors.accent,
header = {
Column(modifier = Modifier.weight(1f)) {
- Text(trackName, style = TrackProType.titleMedium.copy(fontSize = 14.sp), color = TrackProTheme.colors.textPrimary)
+ Text(trackName, style = TrackProType.titleMedium.atSize(14.sp), color = TrackProTheme.colors.textPrimary)
Text(
"$trackMeta · ${sessions.size} sessions",
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = TrackProTheme.colors.textMuted
)
}
@@ -128,8 +138,8 @@ fun ExpandableTrackGroup(
Row(
modifier = Modifier
.fillMaxWidth()
+ .pressable(onClick = { navController.navigate("timeattacklistitem/${session.id}") })
.background(TrackProTheme.colors.bgElevated.copy(alpha = 0.4f), RoundedCornerShape(8.dp))
- .clickable { navController.navigate("timeattacklistitem/${session.id}") }
.padding(Spacing.sm),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
@@ -146,7 +156,7 @@ fun ExpandableTrackGroup(
)
Text(
"Session date: $date",
- style = TrackProType.body.copy(fontSize = 10.sp),
+ style = TrackProType.body.atSize(10.sp),
color = TrackProTheme.colors.textMuted
)
}
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/TrackListView.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/TrackListView.kt
index 897238f..3758e75 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/TrackListView.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/TrackListView.kt
@@ -2,7 +2,6 @@ package com.example.trackpro.screens.listViewScreens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -13,8 +12,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
@@ -41,9 +42,14 @@ import androidx.navigation.NavController
import com.example.trackpro.TrackProApp
import com.example.trackpro.dataClasses.TrackMainData
import com.example.trackpro.extrasForUI.TrackProTheme
-import com.example.trackpro.components.AppTopBar
+import com.example.trackpro.components.isScrolledUnderChrome
+import com.example.trackpro.components.ScreenScaffold
+import com.example.trackpro.components.Haptic
+import com.example.trackpro.components.pressable
+import com.example.trackpro.components.rememberHaptics
import com.example.trackpro.components.EmptyState
import com.example.trackpro.components.StatCell
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
import com.example.trackpro.theme.TrackProType
@@ -63,53 +69,55 @@ fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) {
val database = remember { ESPDatabase.getInstance(context) }
val scope = rememberCoroutineScope()
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(TrackProTheme.colors.bgDeep)
- ) {
- Column(modifier = Modifier.fillMaxSize()) {
-
- AppTopBar(
- title = "My Tracks",
- accent = TrackProTheme.colors.accent,
- trailing = {
- Text(
- text = "${tracks.size} tracks",
- style = TrackProType.label,
- color = TrackProTheme.colors.textMuted
- )
- }
- )
+ val listState = rememberLazyListState()
+ val scrolled by listState.isScrolledUnderChrome()
- if (tracks.isEmpty()) {
- EmptyState(message = "No tracks yet", hint = "Build a track to see it here")
- } else {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(Spacing.md),
- verticalArrangement = Arrangement.spacedBy(Spacing.sm)
- ) {
- items(tracks) { track ->
- TrackCard(
- track = track,
- navController = navController,
- database = database,
- useMetric = useMetric,
- bgCard = TrackProTheme.colors.bgCard,
- bgElevated = TrackProTheme.colors.bgElevated,
- accent = TrackProTheme.colors.accent,
- dangerColor = TrackProTheme.colors.danger,
- textPrimary = TrackProTheme.colors.textPrimary,
- textMuted = TrackProTheme.colors.textMuted,
- sectorLine = TrackProTheme.colors.sectorLine,
- onDelete = { trackToDelete ->
- scope.launch(Dispatchers.IO) {
- database.trackMainDao().deleteTrack(trackToDelete.trackId)
- }
+ ScreenScaffold(
+ title = "My Tracks",
+ onBack = { navController.popBackStack() },
+ accent = TrackProTheme.colors.accent,
+ trailing = {
+ Text(
+ text = "${tracks.size} tracks",
+ style = TrackProType.label,
+ color = TrackProTheme.colors.textMuted
+ )
+ },
+ contentScrolled = scrolled
+ ) { contentPadding ->
+ if (tracks.isEmpty()) {
+ EmptyState(message = "No tracks yet", hint = "Build a track to see it here")
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = listState,
+ contentPadding = PaddingValues(
+ top = contentPadding.calculateTopPadding() + Spacing.md,
+ bottom = Spacing.md,
+ start = Spacing.md,
+ end = Spacing.md
+ ),
+ verticalArrangement = Arrangement.spacedBy(Spacing.sm)
+ ) {
+ items(tracks) { track ->
+ TrackCard(
+ track = track,
+ navController = navController,
+ database = database,
+ useMetric = useMetric,
+ bgCard = TrackProTheme.colors.bgCard,
+ bgElevated = TrackProTheme.colors.bgElevated,
+ accent = TrackProTheme.colors.accent,
+ dangerColor = TrackProTheme.colors.danger,
+ textPrimary = TrackProTheme.colors.textPrimary,
+ textMuted = TrackProTheme.colors.textMuted,
+ sectorLine = TrackProTheme.colors.sectorLine,
+ onDelete = { trackToDelete ->
+ scope.launch(Dispatchers.IO) {
+ database.trackMainDao().deleteTrack(trackToDelete.trackId)
}
- )
- }
+ }
+ )
}
}
}
@@ -132,6 +140,7 @@ fun TrackCard(
onDelete: (TrackMainData) -> Unit
) {
var showDeleteDialog by remember { mutableStateOf(false) }
+ val haptics = rememberHaptics()
var bestLapTime by remember(track.trackId) { mutableStateOf(null) }
LaunchedEffect(track.trackId) {
@@ -147,7 +156,10 @@ fun TrackCard(
titleContentColor = TrackProTheme.colors.textPrimary,
textContentColor = TrackProTheme.colors.textMuted,
confirmButton = {
- TextButton(onClick = { onDelete(track); showDeleteDialog = false }) {
+ TextButton(onClick = {
+ haptics.perform(Haptic.Reject)
+ onDelete(track); showDeleteDialog = false
+ }) {
Text("Delete", color = dangerColor, style = TrackProType.titleMedium)
}
},
@@ -164,9 +176,9 @@ fun TrackCard(
Box(
modifier = Modifier
.fillMaxWidth()
+ .pressable(onClick = { navController.navigate("track/${track.trackId}") })
.background(bgCard, TrackProShapes.card)
.border(1.dp, sectorLine, TrackProShapes.card)
- .clickable { navController.navigate("track/${track.trackId}") }
) {
Column {
// ── Header ────────────────────────────────────
@@ -195,13 +207,13 @@ fun TrackCard(
) {
Text(
text = track.type.uppercase(),
- style = TrackProType.label.copy(fontSize = 9.sp),
+ style = TrackProType.label.atSize(9.sp),
color = textMuted
)
}
Text(
text = track.country.uppercase(),
- style = TrackProType.label.copy(fontSize = 9.sp),
+ style = TrackProType.label.atSize(9.sp),
color = textMuted
)
}
@@ -209,9 +221,10 @@ fun TrackCard(
// Delete button
Box(
modifier = Modifier
+ .minimumInteractiveComponentSize()
+ .pressable(onClick = { showDeleteDialog = true }, scale = 0.90f)
.size(26.dp)
- .background(dangerColor.copy(alpha = 0.1f), TrackProShapes.badge)
- .clickable { showDeleteDialog = true },
+ .background(dangerColor.copy(alpha = 0.1f), TrackProShapes.badge),
contentAlignment = Alignment.Center
) {
Icon(
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/CarViewScreen.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/CarViewScreen.kt
index e1e350b..129bc51 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/CarViewScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/CarViewScreen.kt
@@ -9,9 +9,11 @@ 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.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
@@ -31,7 +33,8 @@ import androidx.compose.ui.unit.dp
import com.example.trackpro.TrackProApp
import com.example.trackpro.dataClasses.VehicleInformationData
import com.example.trackpro.extrasForUI.TrackProTheme
-import com.example.trackpro.components.AppTopBar
+import com.example.trackpro.components.ScreenScaffold
+import com.example.trackpro.components.isScrolledUnderChrome
import com.example.trackpro.components.SectionLabel
import com.example.trackpro.components.StatCell
import com.example.trackpro.components.StatCellSize
@@ -44,7 +47,7 @@ import kotlinx.coroutines.withContext
@Composable
-fun CarViewScreen(vehicleId: Long) {
+fun CarViewScreen(vehicleId: Long, onBack: () -> Unit) {
val context = LocalContext.current
val app = context.applicationContext as TrackProApp
val useMetric by app.useMetricUnits.collectAsState()
@@ -60,11 +63,17 @@ fun CarViewScreen(vehicleId: Long) {
}
}
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(TrackProTheme.colors.bgDeep)
- ) {
+ val listState = rememberLazyListState()
+ val scrolled by listState.isScrolledUnderChrome()
+
+ // The bar now wraps both states - previously it only existed in the loaded branch,
+ // so the screen had no header (and no back affordance) while loading.
+ ScreenScaffold(
+ title = "Vehicle Profile",
+ onBack = onBack,
+ accent = TrackProTheme.colors.accent,
+ contentScrolled = scrolled
+ ) { contentPadding ->
if (vehicleInfo == null) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -79,11 +88,11 @@ fun CarViewScreen(vehicleId: Long) {
}
} else {
val vehicle = vehicleInfo!!
- Column(modifier = Modifier.fillMaxSize()) {
-
- AppTopBar(title = "Vehicle Profile", accent = TrackProTheme.colors.accent)
-
- LazyColumn(modifier = Modifier.fillMaxSize()) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = listState,
+ contentPadding = PaddingValues(top = contentPadding.calculateTopPadding())
+ ) {
// ── Hero ──────────────────────────────
item {
@@ -198,7 +207,6 @@ fun CarViewScreen(vehicleId: Long) {
item { Spacer(Modifier.height(Spacing.xl)) }
}
- }
}
}
}
@@ -237,7 +245,8 @@ private fun VehicleInfoRow(
fun PreviewCarViewScreen()
{
CarViewScreen(
- vehicleId = 1
+ vehicleId = 1,
+ onBack = {}
)
}
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/DragScreenListItem.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/DragScreenListItem.kt
index 957eeca..bde6d02 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/DragScreenListItem.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/DragScreenListItem.kt
@@ -3,7 +3,6 @@ package com.example.trackpro.screens.listViewScreens.listItems
import android.annotation.SuppressLint
import android.view.ViewGroup
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -40,9 +39,12 @@ import com.example.trackpro.managerClasses.utilities.SpeedColorUtils
import com.example.trackpro.managerClasses.utilities.UnitFormatter
import com.example.trackpro.screens.telemetricScreens.DragMetricCard
import com.example.trackpro.screens.telemetricScreens.DragMetricDisplay
+import com.example.trackpro.components.Haptic
+import com.example.trackpro.components.pressable
import com.example.trackpro.components.AppTopBar
import com.example.trackpro.components.StatCell
import com.example.trackpro.components.StatCellSize
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.DataVizColors
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProType
@@ -296,16 +298,20 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
val active = xAxisInMeters == isMeters
Box(
modifier = Modifier
+ .pressable(
+ onClick = { xAxisInMeters = isMeters },
+ scale = 0.96f,
+ haptic = Haptic.Selection
+ )
.background(
if (active) TrackProTheme.colors.accent else TrackProTheme.colors.sectorLine,
RoundedCornerShape(3.dp)
)
- .clickable { xAxisInMeters = isMeters }
.padding(horizontal = 8.dp, vertical = 3.dp)
) {
Text(
text = label,
- style = TrackProType.body.copy(fontSize = 9.sp),
+ style = TrackProType.body.atSize(9.sp),
color = if (active) TrackProTheme.colors.onAccent else TrackProTheme.colors.textMuted
)
}
@@ -315,16 +321,20 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
val active = showMap == isMap
Box(
modifier = Modifier
+ .pressable(
+ onClick = { showMap = isMap },
+ scale = 0.96f,
+ haptic = Haptic.Selection
+ )
.background(
if (active) TrackProTheme.colors.accent else TrackProTheme.colors.sectorLine,
RoundedCornerShape(3.dp)
)
- .clickable { showMap = isMap }
.padding(horizontal = 8.dp, vertical = 3.dp)
) {
Text(
text = label,
- style = TrackProType.body.copy(fontSize = 9.sp),
+ style = TrackProType.body.atSize(9.sp),
color = if (active) TrackProTheme.colors.onAccent else TrackProTheme.colors.textMuted
)
}
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/LapDetailScreen.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/LapDetailScreen.kt
index ad2355f..9464683 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/LapDetailScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/LapDetailScreen.kt
@@ -3,14 +3,8 @@ package com.example.trackpro.screens.listViewScreens.lapDetail
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -18,6 +12,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -37,8 +32,12 @@ import com.example.trackpro.dataClasses.LapInfoData
import com.example.trackpro.dataClasses.LapTimeData
import com.example.trackpro.dataClasses.SectorTimeData
import com.example.trackpro.extrasForUI.TrackProTheme
+import com.example.trackpro.components.Haptic
+import com.example.trackpro.components.pressable
import com.example.trackpro.components.AppTopBar
+import com.example.trackpro.components.DraggableSheet
import com.example.trackpro.components.SectionLabel
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.DataVizColors
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
@@ -208,12 +207,16 @@ fun LapDetailScreen(
val selected = mode == heatmapMode
Box(
modifier = Modifier
+ .pressable(
+ onClick = { heatmapMode = mode },
+ scale = 0.96f,
+ haptic = Haptic.Selection
+ )
.clip(RoundedCornerShape(8.dp))
.background(
if (selected) TrackProTheme.colors.accent
else Color.Transparent
)
- .clickable { heatmapMode = mode }
.padding(horizontal = Spacing.md, vertical = 6.dp),
contentAlignment = Alignment.Center
) {
@@ -250,13 +253,13 @@ fun LapDetailScreen(
.background(COMPARE_COLOR, CircleShape)
)
Column {
- Text("Compare · Lap ${cl.lapnumber}", style = TrackProType.label.copy(fontSize = 8.sp), color = COMPARE_COLOR)
- Text(cl.laptime, style = TrackProType.titleMedium.copy(fontSize = 13.sp), color = TrackProTheme.colors.textPrimary)
+ Text("Compare · Lap ${cl.lapnumber}", style = TrackProType.label.atSize(8.sp), color = COMPARE_COLOR)
+ Text(cl.laptime, style = TrackProType.titleMedium.atSize(13.sp), color = TrackProTheme.colors.textPrimary)
if (compareMs > 0) {
val sign = if (deltaMs > 0) "+" else ""
Text(
text = "${sign}${deltaMs.toLapTimeString()}",
- style = TrackProType.body.copy(fontSize = 10.sp),
+ style = TrackProType.body.atSize(10.sp),
color = if (deltaMs < 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad
)
}
@@ -266,11 +269,15 @@ fun LapDetailScreen(
contentDescription = "Remove compare",
tint = TrackProTheme.colors.textMuted,
modifier = Modifier
+ .minimumInteractiveComponentSize()
+ .pressable(
+ onClick = {
+ compareLap = null
+ compareGps = emptyList()
+ },
+ scale = 0.90f
+ )
.size(16.dp)
- .clickable {
- compareLap = null
- compareGps = emptyList()
- }
)
}
}
@@ -282,11 +289,10 @@ fun LapDetailScreen(
.fillMaxWidth()
.align(Alignment.BottomCenter)
) {
- // Stats panel (expandable)
- AnimatedVisibility(
+ // Stats panel - draggable, so its grab handle means something.
+ DraggableSheet(
visible = showStatsPanel,
- enter = slideInVertically { it } + fadeIn(),
- exit = slideOutVertically { it } + fadeOut()
+ onDismiss = { showStatsPanel = false }
) {
StatsPanel(
primaryLap = lap,
@@ -328,10 +334,10 @@ fun LapDetailScreen(
}
// ── Lap picker bottom sheet ────────────────────
- AnimatedVisibility(
+ DraggableSheet(
visible = showLapPicker,
- enter = slideInVertically { it } + fadeIn(),
- exit = slideOutVertically { it } + fadeOut()
+ onDismiss = { showLapPicker = false },
+ modifier = Modifier.align(Alignment.BottomCenter)
) {
LapPickerSheet(
laps = allSessionLaps.filter { it.id != primaryLapId },
@@ -574,25 +580,12 @@ private fun StatsPanel(
if (it.isEmpty()) 0f else it.average().toFloat()
}
+ // Surface, corner radius and grab handle all come from DraggableSheet now.
Column(
modifier = Modifier
.fillMaxWidth()
- .background(
- TrackProTheme.colors.bgCard.copy(alpha = 0.97f),
- RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
- )
.padding(bottom = 4.dp)
) {
- // Handle
- Box(
- modifier = Modifier
- .align(Alignment.CenterHorizontally)
- .padding(top = 10.dp, bottom = 6.dp)
- .width(40.dp)
- .height(3.dp)
- .background(TrackProTheme.colors.textMuted.copy(alpha = 0.4f), RoundedCornerShape(2.dp))
- )
-
Row(
modifier = Modifier
.fillMaxWidth()
@@ -604,7 +597,10 @@ private fun StatsPanel(
Icon(
Icons.Default.Close, "dismiss",
tint = TrackProTheme.colors.textMuted,
- modifier = Modifier.size(16.dp).clickable { onDismiss() }
+ modifier = Modifier
+ .minimumInteractiveComponentSize()
+ .pressable(onClick = onDismiss, scale = 0.90f)
+ .size(16.dp)
)
}
@@ -617,7 +613,7 @@ private fun StatsPanel(
.padding(horizontal = Spacing.lg, vertical = Spacing.sm),
horizontalArrangement = Arrangement.SpaceBetween
) {
- Text("Metric", style = TrackProType.label.copy(fontSize = 8.sp), color = TrackProTheme.colors.textMuted,
+ Text("Metric", style = TrackProType.label.atSize(8.sp), color = TrackProTheme.colors.textMuted,
modifier = Modifier.weight(1.4f))
Row(
modifier = Modifier.weight(1f),
@@ -625,7 +621,7 @@ private fun StatsPanel(
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(Modifier.size(6.dp).background(PRIMARY_COLOR, CircleShape))
- Text("Lap ${primaryLap.lapnumber}", style = TrackProType.label.copy(fontSize = 8.sp), color = PRIMARY_COLOR)
+ Text("Lap ${primaryLap.lapnumber}", style = TrackProType.label.atSize(8.sp), color = PRIMARY_COLOR)
}
if (compareLap != null) {
Row(
@@ -634,7 +630,7 @@ private fun StatsPanel(
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(Modifier.size(6.dp).background(COMPARE_COLOR, CircleShape))
- Text("Lap ${compareLap.lapnumber}", style = TrackProType.label.copy(fontSize = 8.sp), color = COMPARE_COLOR)
+ Text("Lap ${compareLap.lapnumber}", style = TrackProType.label.atSize(8.sp), color = COMPARE_COLOR)
}
}
}
@@ -676,13 +672,13 @@ private fun StatsPanel(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
- Text(label.uppercase(), style = TrackProType.label.copy(fontSize = 9.sp), color = TrackProTheme.colors.textMuted,
+ Text(label.uppercase(), style = TrackProType.label.atSize(9.sp), color = TrackProTheme.colors.textMuted,
modifier = Modifier.weight(1.4f))
- Text(v1, style = TrackProType.body.copy(fontSize = 13.sp), color = if (label == "Delta" && deltaMs < 0) TrackProTheme.colors.deltaGood
+ Text(v1, style = TrackProType.body.atSize(13.sp), color = if (label == "Delta" && deltaMs < 0) TrackProTheme.colors.deltaGood
else TrackProTheme.colors.textPrimary,
modifier = Modifier.weight(1f))
if (compareLap != null && v2.isNotEmpty()) {
- Text(v2, style = TrackProType.body.copy(fontSize = 13.sp), color = TrackProTheme.colors.textPrimary,
+ Text(v2, style = TrackProType.body.atSize(13.sp), color = TrackProTheme.colors.textPrimary,
modifier = Modifier.weight(1f))
}
}
@@ -710,21 +706,8 @@ private fun LapPickerSheet(
Column(
modifier = Modifier
.fillMaxWidth()
- .background(
- TrackProTheme.colors.bgCard,
- RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
- )
.heightIn(max = 360.dp)
) {
- // Handle + header
- Box(
- modifier = Modifier
- .align(Alignment.CenterHorizontally)
- .padding(top = 10.dp, bottom = 8.dp)
- .width(40.dp)
- .height(3.dp)
- .background(TrackProTheme.colors.textMuted.copy(alpha = 0.4f), RoundedCornerShape(2.dp))
- )
Row(
modifier = Modifier
.fillMaxWidth()
@@ -734,7 +717,10 @@ private fun LapPickerSheet(
) {
SectionLabel("Select Lap to Compare")
Icon(Icons.Default.Close, "close", tint = TrackProTheme.colors.textMuted,
- modifier = Modifier.size(16.dp).clickable { onDismiss() })
+ modifier = Modifier
+ .minimumInteractiveComponentSize()
+ .pressable(onClick = onDismiss, scale = 0.90f)
+ .size(16.dp))
}
HorizontalDivider(color = TrackProTheme.colors.sectorLine)
@@ -749,6 +735,7 @@ private fun LapPickerSheet(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 3.dp)
+ .pressable(onClick = { onSelect(lap) }, haptic = Haptic.Selection)
.clip(RoundedCornerShape(8.dp))
.background(
when {
@@ -766,7 +753,6 @@ private fun LapPickerSheet(
},
shape = RoundedCornerShape(8.dp)
)
- .clickable { onSelect(lap) }
.padding(horizontal = Spacing.md, vertical = Spacing.sm),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
@@ -777,7 +763,7 @@ private fun LapPickerSheet(
) {
Text(
String.format("%02d", lap.lapnumber),
- style = TrackProType.statValue.copy(fontSize = 16.sp),
+ style = TrackProType.statValue.atSize(16.sp),
color = if (isBest) TrackProTheme.colors.accent else TrackProTheme.colors.textPrimary
)
if (isBest) {
@@ -786,7 +772,7 @@ private fun LapPickerSheet(
.background(TrackProTheme.colors.accent.copy(alpha = 0.15f), TrackProShapes.badge)
.padding(horizontal = 5.dp, vertical = 2.dp)
) {
- Text("Best", style = TrackProType.label.copy(fontSize = 7.sp), color = TrackProTheme.colors.accent)
+ Text("Best", style = TrackProType.label.atSize(7.sp), color = TrackProTheme.colors.accent)
}
}
}
@@ -798,10 +784,10 @@ private fun LapPickerSheet(
val sign = if (deltaMs > 0) "+" else ""
Text(
"${sign}${deltaMs.toLapTimeString()}",
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = if (deltaMs < 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad
)
- Text(lap.laptime, style = TrackProType.titleMedium.copy(fontSize = 15.sp), color = TrackProTheme.colors.textPrimary)
+ Text(lap.laptime, style = TrackProType.titleMedium.atSize(15.sp), color = TrackProTheme.colors.textPrimary)
if (isSelected) {
Box(Modifier.size(8.dp).background(COMPARE_COLOR, CircleShape))
}
@@ -841,14 +827,14 @@ private fun ActionButton(
) {
Box(
modifier = modifier
+ .pressable(onClick = onClick, scale = 0.96f)
.clip(RoundedCornerShape(8.dp))
.background(color.copy(alpha = 0.12f))
.border(1.dp, color.copy(alpha = 0.3f), RoundedCornerShape(8.dp))
- .clickable { onClick() }
.padding(vertical = Spacing.sm),
contentAlignment = Alignment.Center
) {
- Text(label, style = TrackProType.titleMedium.copy(fontSize = 12.sp), color = color)
+ Text(label, style = TrackProType.titleMedium.atSize(12.sp), color = color)
}
}
diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/TimeAttackListItem.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/TimeAttackListItem.kt
index 0b2d796..decddb0 100644
--- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/TimeAttackListItem.kt
+++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/TimeAttackListItem.kt
@@ -5,7 +5,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -47,10 +46,12 @@ import com.example.trackpro.dataClasses.LapTimeData
import com.example.trackpro.dataClasses.SessionData
import com.example.trackpro.dataClasses.VehicleInformationData
import com.example.trackpro.extrasForUI.TrackProTheme
+import com.example.trackpro.components.pressable
import com.example.trackpro.components.AppTopBar
import com.example.trackpro.components.SectionLabel
import com.example.trackpro.components.StatCell
import com.example.trackpro.components.StatCellSize
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
import com.example.trackpro.theme.TrackProType
@@ -186,6 +187,7 @@ fun TimeAttackListItemScreen(
item {
AppTopBar(
title = "Session Detail",
+ onBack = { navController.popBackStack() },
accent = TrackProTheme.colors.accent,
trailing = {
Text("${lapTimes.size} laps", style = TrackProType.label, color = TrackProTheme.colors.textMuted)
@@ -215,14 +217,14 @@ fun TimeAttackListItemScreen(
)
Text(
text = "${vehicle.engineType} · ${vehicle.horsepower}hp · ${vehicle.drivetrain}",
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = TrackProTheme.colors.textMuted.copy(alpha = 0.7f)
)
}
Spacer(Modifier.height(4.dp))
Text(
text = DateFormatterUtil.getDateTimeFormat().format(Date(session.startTime)),
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = TrackProTheme.colors.textMuted
)
}
@@ -328,9 +330,9 @@ fun TimeAttackListItemScreen(
val lapMs = lap.laptime.toLapTimeMillis()
val deltaMs = lapMs - bestMs
val topSpeed = topSpeedPerLap[lap.lapnumber] ?: 0f
- Box(modifier = Modifier.clickable {
+ Box(modifier = Modifier.pressable(onClick = {
navController.navigate("lap_detail/$sessionId/${lap.id}")
- }) {
+ })) {
LapRow(
lap = lap,
isBest = isBest,
@@ -411,7 +413,7 @@ private fun LapRow(
) {
Text(
text = String.format("%02d", lap.lapnumber),
- style = TrackProType.statValue.copy(fontSize = 18.sp),
+ style = TrackProType.statValue.atSize(18.sp),
color = accentColor
)
Box(
@@ -419,7 +421,7 @@ private fun LapRow(
.background(accentColor.copy(alpha = 0.15f), TrackProShapes.badge)
.padding(horizontal = 6.dp, vertical = 2.dp)
) {
- Text(badge.uppercase(), style = TrackProType.label.copy(fontSize = 8.sp), color = accentColor)
+ Text(badge.uppercase(), style = TrackProType.label.atSize(8.sp), color = accentColor)
}
}
@@ -436,19 +438,19 @@ private fun LapRow(
Column(horizontalAlignment = Alignment.End) {
Text(
text = lap.laptime,
- style = TrackProType.statValue.copy(fontSize = 17.sp),
+ style = TrackProType.statValue.atSize(17.sp),
color = accentColor
)
if (!isBest && deltaMs > 0) {
Text(
text = "+${deltaMs.toLapTimeString()}",
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = badColor.copy(alpha = 0.8f)
)
} else if (isBest) {
Text(
text = "Reference",
- style = TrackProType.label.copy(fontSize = 9.sp),
+ style = TrackProType.label.atSize(9.sp),
color = goodColor.copy(alpha = 0.7f)
)
}
diff --git a/app/src/main/java/com/example/trackpro/screens/telemetricScreens/DragScreen.kt b/app/src/main/java/com/example/trackpro/screens/telemetricScreens/DragScreen.kt
index c44ed94..9c221dc 100644
--- a/app/src/main/java/com/example/trackpro/screens/telemetricScreens/DragScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/telemetricScreens/DragScreen.kt
@@ -1,7 +1,6 @@
package com.example.trackpro.screens.telemetricScreens
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -61,10 +60,13 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import androidx.core.graphics.toColorInt
import com.example.trackpro.extrasForUI.TrackProTheme
+import com.example.trackpro.components.pressable
+import com.example.trackpro.components.Haptic
import com.example.trackpro.components.AppCard
import com.example.trackpro.components.AppTopBar
import com.example.trackpro.components.PrimaryButton
import com.example.trackpro.components.SectionLabel
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.DataVizColors
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
@@ -81,7 +83,8 @@ data class DragMetricDisplay(
fun DragRaceScreen(
database: ESPDatabase,
sessionManager: SessionManager,
- vehicleViewModel: VehicleFULLViewModel
+ vehicleViewModel: VehicleFULLViewModel,
+ onBack: () -> Unit
) {
val app = LocalContext.current.applicationContext as TrackProApp
val scope = rememberCoroutineScope()
@@ -154,6 +157,7 @@ fun DragRaceScreen(
// 1. TOP STATUS BAR
AppTopBar(
title = "Drag Mode",
+ onBack = onBack,
accent = TrackProTheme.colors.accent,
trailing = {
Row(
@@ -163,7 +167,7 @@ fun DragRaceScreen(
if (isSessionActive) {
Text(
elapsedTime,
- style = TrackProType.statValue.copy(fontSize = 13.sp),
+ style = TrackProType.statValue.atSize(13.sp),
color = TrackProTheme.colors.accent
)
}
@@ -188,7 +192,7 @@ fun DragRaceScreen(
modifier = Modifier
.fillMaxWidth()
.padding(Spacing.md)
- .clickable { showVehicleDropdown = true }
+ .pressable(onClick = { showVehicleDropdown = true })
) {
Row(
Modifier.fillMaxWidth(),
@@ -210,7 +214,7 @@ fun DragRaceScreen(
selectedVehicle?.let {
Text(
"${it.horsepower}hp · ${it.drivetrain} · ${it.year}",
- style = TrackProType.body.copy(fontSize = 11.sp),
+ style = TrackProType.body.atSize(11.sp),
color = TrackProTheme.colors.textMuted
)
}
@@ -238,7 +242,7 @@ fun DragRaceScreen(
)
Text(
"${vehicle.horsepower}hp · ${vehicle.year}",
- style = TrackProType.body.copy(fontSize = 12.sp),
+ style = TrackProType.body.atSize(12.sp),
color = TrackProTheme.colors.textMuted
)
}
@@ -301,7 +305,7 @@ fun DragRaceScreen(
)
Text(
UnitFormatter.speedUnitLabel(useMetric),
- style = TrackProType.body.copy(fontSize = 15.sp),
+ style = TrackProType.body.atSize(15.sp),
color = TrackProTheme.colors.textMuted,
modifier = Modifier.padding(bottom = 8.dp)
)
@@ -311,7 +315,7 @@ fun DragRaceScreen(
Spacer(Modifier.height(8.dp))
Text(
"MAX: ${UnitFormatter.formatSpeed(currentMetrics.maxSpeed, useMetric)} ${UnitFormatter.speedUnitLabel(useMetric)} · DIST: ${UnitFormatter.formatDistance(currentMetrics.totalDistance.toDouble(), useMetric)}",
- style = TrackProType.body.copy(fontSize = 12.sp),
+ style = TrackProType.body.atSize(12.sp),
color = TrackProTheme.colors.textMuted
)
}
@@ -504,6 +508,7 @@ fun DragRaceScreen(
}
},
enabled = selectedVehicle != null,
+ haptic = Haptic.Confirm,
accent = if (isSessionActive) TrackProTheme.colors.bgElevated else TrackProTheme.colors.accent,
contentColor = if (isSessionActive) TrackProTheme.colors.accent else null,
modifier = Modifier.weight(1f).height(56.dp)
@@ -552,13 +557,13 @@ fun DragMetricCard(
) {
Text(
metric.value,
- style = TrackProType.statValue.copy(fontSize = 19.sp),
+ style = TrackProType.statValue.atSize(19.sp),
color = if (metric.achieved) TrackProTheme.colors.textPrimary
else TrackProTheme.colors.textMuted.copy(alpha = 0.5f)
)
Text(
metric.unit,
- style = TrackProType.body.copy(fontSize = 10.sp),
+ style = TrackProType.body.atSize(10.sp),
color = TrackProTheme.colors.textMuted,
modifier = Modifier.padding(bottom = 2.dp)
)
diff --git a/app/src/main/java/com/example/trackpro/screens/telemetricScreens/TimeAttackScreen.kt b/app/src/main/java/com/example/trackpro/screens/telemetricScreens/TimeAttackScreen.kt
index 9d6dc62..beebc14 100644
--- a/app/src/main/java/com/example/trackpro/screens/telemetricScreens/TimeAttackScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/telemetricScreens/TimeAttackScreen.kt
@@ -41,10 +41,13 @@ import com.example.trackpro.dataClasses.LatLonOffset
import com.example.trackpro.extrasForUI.TrackProTheme
import com.example.trackpro.managerClasses.timeAttackManagers.SectorSplit
import com.example.trackpro.managerClasses.timeAttackManagers.TimingMode
+import com.example.trackpro.components.Haptic
+import com.example.trackpro.components.rememberHaptics
import com.example.trackpro.components.AppTopBar
import com.example.trackpro.components.StatCell
import com.example.trackpro.components.StatCellDivider
import com.example.trackpro.components.StatCellSize
+import com.example.trackpro.theme.atSize
import com.example.trackpro.theme.DataVizColors
import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProShapes
@@ -75,6 +78,7 @@ import kotlin.math.sin
fun TimeAttackScreenView(
trackId: Long? = null,
vehicleId: Long? = null,
+ onBack: () -> Unit
) {
val context = LocalContext.current
val app = context.applicationContext as TrackProApp
@@ -159,6 +163,14 @@ fun TimeAttackScreenView(
} ?: Log.w("TimeAttackScreen", "GPS data is null")
}
+ // The single most useful haptic in the app: a lap closing is confirmed by feel, so
+ // the driver doesn't have to look away from the track to know it registered.
+ // Keyed on the counter itself, so it fires exactly once per lap.
+ val haptics = rememberHaptics()
+ LaunchedEffect(eventCount) {
+ if (eventCount > 0) haptics.perform(Haptic.Confirm)
+ }
+
val gpsPoints = fullTrack //+ linesToShow
val driverPos = driver ?: LatLonOffset(0.0, 0.0)
@@ -176,7 +188,8 @@ fun TimeAttackScreenView(
driver = driverPos,
isConnected = isConnected,
linesToShow = linesToShow,
- lapSplits = lapSplits
+ lapSplits = lapSplits,
+ onBack = onBack
)
else -> TimeAttackPortraitLayout(
timingMode = timingMode,
@@ -191,7 +204,8 @@ fun TimeAttackScreenView(
driver = driverPos,
isConnected = isConnected,
linesToShow = linesToShow,
- lapSplits = lapSplits
+ lapSplits = lapSplits,
+ onBack = onBack
)
}
@@ -212,7 +226,8 @@ fun TimeAttackPortraitLayout(
driver: LatLonOffset,
isConnected: Boolean,
linesToShow : List,
- lapSplits: List = emptyList()
+ lapSplits: List = emptyList(),
+ onBack: () -> Unit
) {
val deltaColor = if (delta <= 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad
val eventName = if (timingMode is TimingMode.Circuit) "LAP" else "RUN"
@@ -228,6 +243,7 @@ fun TimeAttackPortraitLayout(
AppTopBar(
title = "$modeLabel Mode",
+ onBack = onBack,
accent = modeColor,
trailing = {
Text(
@@ -264,7 +280,7 @@ fun TimeAttackPortraitLayout(
) {
Text(
text = "Δ ${String.format("%+.3f", delta)}s",
- style = TrackProType.statValue.copy(fontSize = 15.sp),
+ style = TrackProType.statValue.atSize(15.sp),
color = deltaColor
)
}
@@ -355,7 +371,8 @@ fun TimeAttackLandscapeLayout(
driver: LatLonOffset,
isConnected: Boolean,
linesToShow: List,
- lapSplits: List = emptyList()
+ lapSplits: List = emptyList(),
+ onBack: () -> Unit
) {
val deltaColor = if (delta <= 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad
val eventName = if (timingMode is TimingMode.Circuit) "LAP" else "RUN"
@@ -376,6 +393,7 @@ fun TimeAttackLandscapeLayout(
) {
AppTopBar(
title = modeLabel,
+ onBack = onBack,
accent = modeColor,
trailing = {
Text(
@@ -405,7 +423,7 @@ fun TimeAttackLandscapeLayout(
) {
Text(
text = "Δ ${String.format("%+.3f", delta)}s",
- style = TrackProType.statValue.copy(fontSize = 13.sp),
+ style = TrackProType.statValue.atSize(13.sp),
color = deltaColor
)
}
@@ -516,12 +534,12 @@ private fun SectorSplitsRow(splits: List) {
Column {
Text(
"S${split.sectorIndex + 1}",
- style = TrackProType.label.copy(fontSize = 9.sp, letterSpacing = 0.5.sp),
+ style = TrackProType.label.atSize(9.sp).copy(letterSpacing = 0.5.sp),
color = TrackProTheme.colors.textFaint
)
Text(
String.format("%.2fs", split.splitMs / 1000.0),
- style = TrackProType.statValue.copy(fontSize = 13.sp),
+ style = TrackProType.statValue.atSize(13.sp),
color = deltaColor
)
if (split.deltaMs != null) {
@@ -529,7 +547,7 @@ private fun SectorSplitsRow(splits: List) {
val sign = if (deltaSeconds > 0) "+" else ""
Text(
"$sign${String.format("%.2f", deltaSeconds)}",
- style = TrackProType.body.copy(fontSize = 9.sp),
+ style = TrackProType.body.atSize(9.sp),
color = deltaColor
)
}
diff --git a/app/src/main/java/com/example/trackpro/screens/vehicleScreens/CarCreatorScreen.kt b/app/src/main/java/com/example/trackpro/screens/vehicleScreens/CarCreatorScreen.kt
index 2c9a2c4..30e5603 100644
--- a/app/src/main/java/com/example/trackpro/screens/vehicleScreens/CarCreatorScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/vehicleScreens/CarCreatorScreen.kt
@@ -1,11 +1,8 @@
package com.example.trackpro.screens.vehicleScreens
import android.widget.Toast
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.padding
@@ -39,7 +36,8 @@ import com.example.trackpro.extrasForUI.AppDropdownField
import com.example.trackpro.extrasForUI.CustomTextField
import com.example.trackpro.extrasForUI.TrackProTheme
import com.example.trackpro.components.AppCard
-import com.example.trackpro.components.AppTopBar
+import com.example.trackpro.components.ScreenScaffold
+import com.example.trackpro.components.isScrolledUnderChrome
import com.example.trackpro.components.PrimaryButton
import com.example.trackpro.components.SectionLabel
import com.example.trackpro.theme.Spacing
@@ -49,7 +47,8 @@ import kotlinx.coroutines.launch
@Composable
fun CarCreationScreen(
- database: ESPDatabase
+ database: ESPDatabase,
+ onBack: () -> Unit
) {
val context = LocalContext.current
val app = context.applicationContext as TrackProApp
@@ -76,88 +75,91 @@ fun CarCreationScreen(
val scrollState = rememberScrollState()
- Box(
+ val scrolled by scrollState.isScrolledUnderChrome()
+
+ ScreenScaffold(
+ title = "Vehicle Setup",
+ onBack = onBack,
+ accent = TrackProTheme.colors.accent,
+ contentScrolled = scrolled
+ ) { contentPadding ->
+ Column(
modifier = Modifier
- .fillMaxSize()
- .background(TrackProTheme.colors.bgDeep)
+ .verticalScroll(scrollState)
+ .fillMaxWidth()
+ .padding(
+ top = contentPadding.calculateTopPadding() + Spacing.md,
+ start = Spacing.md,
+ end = Spacing.md,
+ bottom = Spacing.md
+ ),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- Column(modifier = Modifier.fillMaxSize()) {
-
- AppTopBar(title = "Vehicle Setup", accent = TrackProTheme.colors.accent)
-
- Column(
- modifier = Modifier
- .verticalScroll(scrollState)
- .fillMaxWidth()
- .padding(Spacing.md),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- AppCard(modifier = Modifier.fillMaxWidth(), padding = 20.dp) {
-
- SectionLabel("Basic Info (Required)", modifier = Modifier.padding(vertical = Spacing.sm))
- CustomTextField("Manufacturer", manufacturer, leadingIcon = Icons.Default.Business) { manufacturer = it }
- CustomTextField("Model", model, leadingIcon = Icons.Default.DirectionsCar) { model = it }
- CustomTextField("Year", year, leadingIcon = Icons.Default.Event) { year = it }
-
- SectionLabel("Performance", modifier = Modifier.padding(vertical = Spacing.sm))
- CustomTextField("Horsepower", horsepower, true, Icons.Default.FlashOn) { horsepower = it }
- CustomTextField("Torque (Nm)", torque, true, Icons.Default.Settings) { torque = it }
- CustomTextField("Weight (kg)", weight, true, Icons.Default.FitnessCenter) { weight = it }
- CustomTextField("Top Speed (${UnitFormatter.speedUnitLabel(useMetric)})", topSpeed, true, Icons.Default.Speed) { topSpeed = it }
- CustomTextField(
- if (useMetric) "0-100 KM/H (s)" else "0-60 MPH (s)",
- acceleration, true, Icons.Default.Timer
- ) { acceleration = it }
- CustomTextField("Fuel Capacity (L)", fuelCapacity, true, Icons.Default.LocalGasStation) { fuelCapacity = it }
-
- SectionLabel("Configuration", modifier = Modifier.padding(vertical = Spacing.sm))
- AppDropdownField("Engine Type", jsonOptions.engineTypes, selectedEngineType, { it }, { selectedEngineType = it })
- AppDropdownField("Drivetrain", jsonOptions.drivetrains, selectedDrivetrain, { it }, { selectedDrivetrain = it })
- AppDropdownField("Fuel Type", jsonOptions.fuelTypes, selectedFuelType, { it }, { selectedFuelType = it })
- AppDropdownField("Tire Type", jsonOptions.tireTypes, selectedTireType, { it }, { selectedTireType = it })
- AppDropdownField("Transmission", jsonOptions.transmissions, selectedTransmission, { it }, { selectedTransmission = it })
- AppDropdownField("Suspension", jsonOptions.suspensionTypes, selectedSuspensionType, { it }, { selectedSuspensionType = it })
-
- Spacer(modifier = Modifier.height(Spacing.md))
-
- PrimaryButton(
- text = "Save Vehicle",
- onClick = {
- if (manufacturer.isBlank() || model.isBlank() || year.isBlank()) {
- Toast.makeText(context, "Fill in required fields.", Toast.LENGTH_SHORT).show()
- return@PrimaryButton
- }
-
- val vehicle = VehicleInformationData(
- manufacturer = manufacturer,
- model = model,
- year = year.toIntOrNull() ?: 0,
- engineType = selectedEngineType,
- horsepower = horsepower.toIntOrNull() ?: 0,
- torque = torque.toIntOrNull(),
- weight = weight.toDoubleOrNull() ?: 0.0,
- // Stored canonically in km/h regardless of the unit the
- // user entered it in, matching every other speed value.
- topSpeed = topSpeed.toDoubleOrNull()?.let { UnitFormatter.convertSpeedToKmh(it, useMetric) },
- acceleration = acceleration.toDoubleOrNull(),
- drivetrain = selectedDrivetrain,
- fuelType = selectedFuelType,
- tireType = selectedTireType,
- fuelCapacity = fuelCapacity.toDoubleOrNull(),
- transmission = selectedTransmission,
- suspensionType = selectedSuspensionType
- )
-
- coroutineScope.launch {
- database.vehicleInformationDAO().insertVehicle(vehicle)
- }
-
- Toast.makeText(context, "Vehicle saved successfully.", Toast.LENGTH_SHORT).show()
- },
- accent = TrackProTheme.colors.deltaGood,
- modifier = Modifier.fillMaxWidth()
- )
- }
+ AppCard(modifier = Modifier.fillMaxWidth(), padding = 20.dp) {
+
+ SectionLabel("Basic Info (Required)", modifier = Modifier.padding(vertical = Spacing.sm))
+ CustomTextField("Manufacturer", manufacturer, leadingIcon = Icons.Default.Business) { manufacturer = it }
+ CustomTextField("Model", model, leadingIcon = Icons.Default.DirectionsCar) { model = it }
+ CustomTextField("Year", year, leadingIcon = Icons.Default.Event) { year = it }
+
+ SectionLabel("Performance", modifier = Modifier.padding(vertical = Spacing.sm))
+ CustomTextField("Horsepower", horsepower, true, Icons.Default.FlashOn) { horsepower = it }
+ CustomTextField("Torque (Nm)", torque, true, Icons.Default.Settings) { torque = it }
+ CustomTextField("Weight (kg)", weight, true, Icons.Default.FitnessCenter) { weight = it }
+ CustomTextField("Top Speed (${UnitFormatter.speedUnitLabel(useMetric)})", topSpeed, true, Icons.Default.Speed) { topSpeed = it }
+ CustomTextField(
+ if (useMetric) "0-100 KM/H (s)" else "0-60 MPH (s)",
+ acceleration, true, Icons.Default.Timer
+ ) { acceleration = it }
+ CustomTextField("Fuel Capacity (L)", fuelCapacity, true, Icons.Default.LocalGasStation) { fuelCapacity = it }
+
+ SectionLabel("Configuration", modifier = Modifier.padding(vertical = Spacing.sm))
+ AppDropdownField("Engine Type", jsonOptions.engineTypes, selectedEngineType, { it }, { selectedEngineType = it })
+ AppDropdownField("Drivetrain", jsonOptions.drivetrains, selectedDrivetrain, { it }, { selectedDrivetrain = it })
+ AppDropdownField("Fuel Type", jsonOptions.fuelTypes, selectedFuelType, { it }, { selectedFuelType = it })
+ AppDropdownField("Tire Type", jsonOptions.tireTypes, selectedTireType, { it }, { selectedTireType = it })
+ AppDropdownField("Transmission", jsonOptions.transmissions, selectedTransmission, { it }, { selectedTransmission = it })
+ AppDropdownField("Suspension", jsonOptions.suspensionTypes, selectedSuspensionType, { it }, { selectedSuspensionType = it })
+
+ Spacer(modifier = Modifier.height(Spacing.md))
+
+ PrimaryButton(
+ text = "Save Vehicle",
+ onClick = {
+ if (manufacturer.isBlank() || model.isBlank() || year.isBlank()) {
+ Toast.makeText(context, "Fill in required fields.", Toast.LENGTH_SHORT).show()
+ return@PrimaryButton
+ }
+
+ val vehicle = VehicleInformationData(
+ manufacturer = manufacturer,
+ model = model,
+ year = year.toIntOrNull() ?: 0,
+ engineType = selectedEngineType,
+ horsepower = horsepower.toIntOrNull() ?: 0,
+ torque = torque.toIntOrNull(),
+ weight = weight.toDoubleOrNull() ?: 0.0,
+ // Stored canonically in km/h regardless of the unit the
+ // user entered it in, matching every other speed value.
+ topSpeed = topSpeed.toDoubleOrNull()?.let { UnitFormatter.convertSpeedToKmh(it, useMetric) },
+ acceleration = acceleration.toDoubleOrNull(),
+ drivetrain = selectedDrivetrain,
+ fuelType = selectedFuelType,
+ tireType = selectedTireType,
+ fuelCapacity = fuelCapacity.toDoubleOrNull(),
+ transmission = selectedTransmission,
+ suspensionType = selectedSuspensionType
+ )
+
+ coroutineScope.launch {
+ database.vehicleInformationDAO().insertVehicle(vehicle)
+ }
+
+ Toast.makeText(context, "Vehicle saved successfully.", Toast.LENGTH_SHORT).show()
+ },
+ accent = TrackProTheme.colors.deltaGood,
+ modifier = Modifier.fillMaxWidth()
+ )
}
}
}