diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e2d6409..5788ed7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,5 +38,9 @@
+
+
+
+
\ 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 f870530..b70d201 100644
--- a/app/src/main/java/com/example/trackpro/MainActivity.kt
+++ b/app/src/main/java/com/example/trackpro/MainActivity.kt
@@ -3,6 +3,7 @@ package com.example.trackpro
import com.example.trackpro.extrasForUI.TrackProTheme
import android.Manifest
import android.app.Application
+import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
@@ -62,10 +63,12 @@ import androidx.navigation.navArgument
import com.example.trackpro.managerClasses.ESPDatabase
import com.example.trackpro.managerClasses.TrackSeeder
import com.example.trackpro.managerClasses.gpsDataManagers.ESPTcpClient
+import com.example.trackpro.managerClasses.gpsDataManagers.BluetoothClassicClient
import com.example.trackpro.managerClasses.JsonReader
import com.example.trackpro.managerClasses.SessionManager
import com.example.trackpro.managerClasses.gpsDataManagers.GpsManager
import com.example.trackpro.managerClasses.gpsDataManagers.PhoneGpsProvider
+import com.example.trackpro.models.GpsProviderType
import com.example.trackpro.screens.vehicleScreens.CarCreationScreen
import com.example.trackpro.screens.telemetricScreens.DragRaceScreen
import com.example.trackpro.screens.ESPConnectionTestScreen
@@ -114,11 +117,76 @@ class TrackProApp : Application() {
ESPTcpClient(serverAddress = config.first, port = config.second)
}
+ // Lets Settings redirect the WiFi connection to a test simulator (e.g.
+ // esp32_simulator.py on a dev machine) instead of the real ESP32's fixed
+ // AP address, without editing config.json and rebuilding. Port always
+ // comes from config.json (the simulator listens on the same 4210 the
+ // firmware does) - only the host is swappable.
+ private val espTargetPrefs by lazy { getSharedPreferences("esp_target_prefs", MODE_PRIVATE) }
+ val useTestServer by lazy { MutableStateFlow(espTargetPrefs.getBoolean("use_test_server", false)) }
+ val testServerAddress by lazy { MutableStateFlow(espTargetPrefs.getString("test_server_address", "") ?: "") }
+
+ fun setUseTestServer(enabled: Boolean) {
+ espTargetPrefs.edit().putBoolean("use_test_server", enabled).apply()
+ useTestServer.value = enabled
+ applyEspTarget()
+ }
+
+ fun setTestServerAddress(address: String) {
+ espTargetPrefs.edit().putString("test_server_address", address).apply()
+ testServerAddress.value = address
+ if (useTestServer.value) applyEspTarget()
+ }
+
+ private fun applyEspTarget() {
+ val (realIp, port) = JsonReader.loadConfig(this)
+ val target = if (useTestServer.value && testServerAddress.value.isNotBlank()) {
+ testServerAddress.value
+ } else {
+ realIp
+ }
+ espTcpClient.updateTarget(target, port)
+ }
+
+ val bluetoothClassicClient: BluetoothClassicClient by lazy {
+ BluetoothClassicClient(this)
+ }
+
val phoneGpsProvider: PhoneGpsProvider by lazy {
PhoneGpsProvider(this)
}
- val useExternalGps = MutableStateFlow(true)
+ // Persisted like useDarkTheme/useMetricUnits below (unlike the old useExternalGps,
+ // which reset to WiFi every launch) — avoids surprising the user mid-track-day.
+ private val gpsSourcePrefs by lazy { getSharedPreferences("gps_source_prefs", MODE_PRIVATE) }
+ val gpsSource by lazy {
+ val stored = gpsSourcePrefs.getString("source", GpsProviderType.WIFI.name)
+ val initial = runCatching { GpsProviderType.valueOf(stored ?: GpsProviderType.WIFI.name) }
+ .getOrDefault(GpsProviderType.WIFI)
+ MutableStateFlow(initial)
+ }
+
+ fun setGpsSource(source: GpsProviderType) {
+ gpsSourcePrefs.edit().putString("source", source.name).apply()
+ gpsSource.value = source
+ }
+
+ private val ratePrefs by lazy { getSharedPreferences("gps_rate_prefs", MODE_PRIVATE) }
+ val selectedRateHz by lazy { MutableStateFlow(ratePrefs.getInt("rate_hz", 10)) }
+
+ fun setRateHz(hz: Int) {
+ ratePrefs.edit().putInt("rate_hz", hz).apply()
+ selectedRateHz.value = hz
+ gpsManager.sendCommandToActive("RATE:$hz\n")
+ }
+
+ private val btDevicePrefs by lazy { getSharedPreferences("bluetooth_prefs", MODE_PRIVATE) }
+ val selectedBtDeviceMac by lazy { MutableStateFlow(btDevicePrefs.getString("device_mac", null)) }
+
+ fun setSelectedBtDevice(mac: String) {
+ btDevicePrefs.edit().putString("device_mac", mac).apply()
+ selectedBtDeviceMac.value = mac
+ }
private val themePrefs by lazy { getSharedPreferences("theme_prefs", MODE_PRIVATE) }
val useDarkTheme by lazy { MutableStateFlow(themePrefs.getBoolean("dark_theme", true)) }
@@ -138,15 +206,21 @@ class TrackProApp : Application() {
val gpsManager: GpsManager by lazy {
GpsManager(
- espProvider = espTcpClient,
+ wifiProvider = espTcpClient,
+ bluetoothProvider = bluetoothClassicClient,
phoneProvider = phoneGpsProvider,
- useExternalGps = useExternalGps
+ gpsSource = gpsSource,
+ selectedRateHz = selectedRateHz
)
}
override fun onCreate() {
super.onCreate()
MapLibre.getInstance(this)
+ // Apply a persisted test-server redirect (if any) before the first
+ // connection attempt, so a restart doesn't briefly dial the real ESP32
+ // before switching over.
+ applyEspTarget()
// Start the active provider immediately at app launch
gpsManager.startActiveProvider()
@@ -176,6 +250,23 @@ class MainActivity : ComponentActivity() {
}
}
+ // Requested contextually (only when the user opens the Bluetooth device
+ // picker in Settings), unlike the eager location request above — Bluetooth
+ // is opt-in/rare, location is core to the app on every launch.
+ private val bluetoothPermissionRequest = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ if (permissions[Manifest.permission.BLUETOOTH_CONNECT] != true) {
+ Log.w("Permissions", "Bluetooth permission denied — Bluetooth GPS source unavailable")
+ }
+ }
+
+ private fun requestBluetoothPermissionIfNeeded() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ bluetoothPermissionRequest.launch(arrayOf(Manifest.permission.BLUETOOTH_CONNECT))
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -220,7 +311,7 @@ class MainActivity : ComponentActivity() {
DragRaceScreen(database, sessionManager, vehicleFULLViewModel)
}
composable("esptest") {
- ESPConnectionTestScreen()
+ ESPConnectionTestScreen(onNavigateToSettings = { navController.navigate("settings") })
}
composable(
"track/{trackId}",
@@ -286,7 +377,10 @@ class MainActivity : ComponentActivity() {
)
}
composable(route = "settings") {
- SettingsScreen(onBack = { navController.popBackStack() })
+ SettingsScreen(
+ onBack = { navController.popBackStack() },
+ onRequestBluetoothPermission = { requestBluetoothPermissionIfNeeded() }
+ )
}
// In your NavHost setup
composable("lap_detail/{sessionId}/{lapId}") { backStackEntry ->
@@ -337,7 +431,7 @@ fun MainScreen(
Box(
modifier = Modifier
.size(6.dp)
- .background(TrackProTheme.colors.accentCyan, CircleShape)
+ .background(TrackProTheme.colors.accent, CircleShape)
)
Spacer(Modifier.width(10.dp))
Column {
@@ -361,13 +455,13 @@ fun MainScreen(
DrawerItem(
icon = Icons.Default.RocketLaunch,
label = "Drag Sessions",
- tint = TrackProTheme.colors.accentCyan,
+ tint = TrackProTheme.colors.accentMuted,
onClick = { onNavigateToDragTimesList(); scope.launch { drawerState.close() } }
)
DrawerItem(
icon = Icons.Default.FlagCircle,
label = "Track Sessions",
- tint = TrackProTheme.colors.accentBlue,
+ tint = TrackProTheme.colors.accentMuted,
onClick = { onNavigateToTimeAttackListView(); scope.launch { drawerState.close() } }
)
}
@@ -382,13 +476,13 @@ fun MainScreen(
DrawerItem(
icon = Icons.Default.Timelapse,
label = "My Tracks",
- tint = TrackProTheme.colors.accentAmber,
+ tint = TrackProTheme.colors.accentMuted,
onClick = { onNavigateToTrackListScreen(); scope.launch { drawerState.close() } }
)
DrawerItem(
icon = Icons.Default.CarRepair,
label = "My Vehicles",
- tint = TrackProTheme.colors.accentAmber,
+ tint = TrackProTheme.colors.accentMuted,
onClick = { onNavigateToVehicleList(); scope.launch { drawerState.close() } }
)
}
@@ -463,7 +557,7 @@ fun MainScreen(
modifier = Modifier
.width(28.dp)
.height(2.dp)
- .background(TrackProTheme.colors.accentCyan)
+ .background(TrackProTheme.colors.accent)
)
Spacer(Modifier.height(10.dp))
Text(
@@ -496,7 +590,7 @@ fun MainScreen(
icon = Icons.Default.RocketLaunch,
title = "DRAG TIMING",
subtitle = "0–100 · ¼ mile · speed trace",
- accentColor = TrackProTheme.colors.accentCyan,
+ accentColor = TrackProTheme.colors.accentMuted,
onClick = onNavigateToDragRace,
fullWidth = true
)
@@ -505,7 +599,7 @@ fun MainScreen(
icon = Icons.Default.FlagCircle,
title = "LAP TIMING",
subtitle = "Circuit & sprint · live delta · best lap",
- accentColor = TrackProTheme.colors.accentBlue,
+ accentColor = TrackProTheme.colors.accentMuted,
onClick = onNavigateToTrackVehicleSelector,
fullWidth = true
)
@@ -526,7 +620,7 @@ fun MainScreen(
icon = Icons.Default.CarRepair,
title = "ADD VEHICLES",
subtitle = "Create your own vehicles",
- accentColor = TrackProTheme.colors.accentAmber,
+ accentColor = TrackProTheme.colors.accentMuted,
onClick = onNavigateToVehicleCreatorScreen,
halfWidth = true
)
@@ -536,7 +630,7 @@ fun MainScreen(
icon = Icons.Default.Timelapse,
title = "TRACK\nBUILDER",
subtitle = "Define tracks",
- accentColor = TrackProTheme.colors.accentAmber,
+ accentColor = TrackProTheme.colors.accentMuted,
onClick = onNavigateToTrackBuilder,
halfWidth = true
)
diff --git a/app/src/main/java/com/example/trackpro/dataClasses/RawGPSData.kt b/app/src/main/java/com/example/trackpro/dataClasses/RawGPSData.kt
index 1a11584..cc9a8ce 100644
--- a/app/src/main/java/com/example/trackpro/dataClasses/RawGPSData.kt
+++ b/app/src/main/java/com/example/trackpro/dataClasses/RawGPSData.kt
@@ -25,6 +25,7 @@ data class RawGPSData(
val altitude: Double?,
val timestamp: Long,
val speed: Float?,
- val fixQuality: Int?
+ val fixQuality: Int?,
+ val valid: Boolean? = null
)
diff --git a/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt b/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt
index 79871e9..46adf9c 100644
--- a/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt
+++ b/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt
@@ -16,7 +16,7 @@ fun CustomTextField(
value: String,
isNumber: Boolean = false,
leadingIcon: ImageVector? = null,
- accent: Color = TrackProTheme.colors.accentCyan,
+ accent: Color = TrackProTheme.colors.accent,
onValueChange: (String) -> Unit
) {
OutlinedTextField(
diff --git a/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt b/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt
index 07a7788..82b2167 100644
--- a/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt
+++ b/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt
@@ -36,7 +36,7 @@ fun AppDropdownField(
itemLabel: (T) -> String,
onSelect: (T) -> Unit,
modifier: Modifier = Modifier,
- accent: Color = TrackProTheme.colors.accentCyan,
+ accent: Color = TrackProTheme.colors.accent,
emptyMessage: String = "No options available"
) {
var expanded by remember { mutableStateOf(false) }
@@ -103,7 +103,7 @@ fun DropdownMenuFieldMulti(label: String, options: List, selectedOp
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = TrackProTheme.colors.textPrimary,
unfocusedTextColor = TrackProTheme.colors.textPrimary,
- focusedBorderColor = TrackProTheme.colors.accentCyan,
+ focusedBorderColor = TrackProTheme.colors.accent,
unfocusedBorderColor = TrackProTheme.colors.sectorLine
),
modifier = Modifier
@@ -151,7 +151,7 @@ fun TrackDropdownMenu(
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = TrackProTheme.colors.textPrimary,
unfocusedTextColor = TrackProTheme.colors.textPrimary,
- focusedBorderColor = TrackProTheme.colors.accentCyan,
+ focusedBorderColor = TrackProTheme.colors.accent,
unfocusedBorderColor = TrackProTheme.colors.sectorLine
),
modifier = Modifier.fillMaxWidth()
diff --git a/app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt b/app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt
index 835197b..9d94e47 100644
--- a/app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt
+++ b/app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt
@@ -32,12 +32,15 @@ object TrackProTheme {
private fun TrackProColorScheme.toMaterialColorScheme(dark: Boolean): androidx.compose.material3.ColorScheme =
if (dark) {
darkColorScheme(
- primary = accentCyan,
- onPrimary = Color.Black,
- secondary = accentBlue,
- onSecondary = Color.Black,
- tertiary = accentAmber,
- onTertiary = Color.Black,
+ // primary/secondary/tertiary intentionally all map to the same accent - the
+ // palette has exactly one. Material needs three slots filled; that is not a
+ // reason to invent two more brand colors.
+ primary = accent,
+ onPrimary = onAccent,
+ secondary = accent,
+ onSecondary = onAccent,
+ tertiary = accent,
+ onTertiary = onAccent,
background = bgDeep,
onBackground = textPrimary,
surface = bgCard,
@@ -46,16 +49,16 @@ private fun TrackProColorScheme.toMaterialColorScheme(dark: Boolean): androidx.c
onSurfaceVariant = textMuted,
outline = sectorLine,
error = deltaBad,
- onError = Color.White
+ onError = Color.Black
)
} else {
lightColorScheme(
- primary = accentCyan,
- onPrimary = Color.Black,
- secondary = accentBlue,
- onSecondary = Color.Black,
- tertiary = accentAmber,
- onTertiary = Color.Black,
+ primary = accent,
+ onPrimary = onAccent,
+ secondary = accent,
+ onSecondary = onAccent,
+ tertiary = accent,
+ onTertiary = onAccent,
background = bgDeep,
onBackground = textPrimary,
surface = bgCard,
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt b/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt
index 900e7a2..269c30c 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt
@@ -37,7 +37,7 @@ import com.example.trackpro.dataClasses.VehicleInformationData
LapTimeData::class,
LapInfoData::class,
SectorTimeData::class
-], version = 3, exportSchema = false)
+], version = 4, exportSchema = false)
abstract class ESPDatabase : RoomDatabase() {
abstract fun sessionDataDao(): SessionDataDao
abstract fun rawGPSDataDao(): RawGPSDataDao
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/ESPTcpClient.kt b/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/ESPTcpClient.kt
index 90b354a..03349a1 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/ESPTcpClient.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/ESPTcpClient.kt
@@ -2,7 +2,7 @@ package com.example.trackpro.managerClasses.gpsDataManagers
import android.util.Log
import com.example.trackpro.dataClasses.RawGPSData
-import com.example.trackpro.models.GpsProvider
+import com.example.trackpro.models.CommandableGpsProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -10,24 +10,31 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import java.io.ByteArrayOutputStream
-import java.io.InputStream
+import java.io.OutputStream
import java.net.InetSocketAddress
import java.net.Socket
import java.util.concurrent.atomic.AtomicBoolean
class ESPTcpClient(
- private val serverAddress: String,
- private val port: Int
-) : GpsProvider {
+ serverAddress: String,
+ port: Int
+) : CommandableGpsProvider {
+ // Mutable (not the constructor params directly) so updateTarget() can
+ // redirect a live singleton - e.g. switching between the real ESP32 and
+ // a test simulator from Settings - without restarting the app.
+ @Volatile private var serverAddress: String = serverAddress
+ @Volatile private var port: Int = port
+
// --- Observables (Singletons use these instead of callbacks) ---
private val _connectionStatus = MutableStateFlow(false)
override val connectionStatus: StateFlow = _connectionStatus.asStateFlow()
private val _gpsFlow = MutableStateFlow(null)
override val gpsFlow: StateFlow = _gpsFlow.asStateFlow()
+ private val _confirmedRateHz = MutableStateFlow(null)
+ override val confirmedRateHz: StateFlow = _confirmedRateHz.asStateFlow()
override fun start() = connect()
@@ -38,17 +45,26 @@ class ESPTcpClient(
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var socket: Socket? = null
private var running = AtomicBoolean(false)
-
- private val json = Json {
- ignoreUnknownKeys = true
- isLenient = true
- coerceInputValues = true
- }
+ @Volatile private var outputStream: OutputStream? = null
+ private val writeMutex = Mutex()
private val bufferPool = BufferPool(512, 10)
// --- Core Methods ---
+ // Redirects future connections to a different host/port. If currently
+ // connected, reconnects immediately to the new target instead of waiting
+ // for the next manual start().
+ fun updateTarget(address: String, newPort: Int) {
+ val changed = serverAddress != address || port != newPort
+ serverAddress = address
+ port = newPort
+ if (changed && running.get()) {
+ disconnect()
+ connect()
+ }
+ }
+
fun connect() {
if (running.getAndSet(true)) return
@@ -59,6 +75,7 @@ class ESPTcpClient(
socket?.connect(InetSocketAddress(serverAddress, port), 5000)
val inputStream = socket?.getInputStream() ?: throw Exception("Failed to get input stream")
+ outputStream = socket?.getOutputStream()
_connectionStatus.value = true
val delimiter = "\n".toByteArray()
@@ -86,37 +103,54 @@ class ESPTcpClient(
}
}
- private suspend fun processChunk(buffer: ByteArray, length: Int) {
- val message = buffer.decodeToString(0, length).trim()
- if (message.isNotEmpty()) {
- withContext(Dispatchers.Default) {
+ // Sends a command (e.g. "RATE:20\n") to the ESP32. Dispatches onto this
+ // client's own IO scope so callers (Compose onClick handlers) never block,
+ // and a Mutex serializes concurrent writers against each other - reading
+ // and writing the same socket concurrently is safe by contract and needs
+ // no lock between them.
+ override fun sendCommand(command: String) {
+ val out = outputStream ?: return
+ scope.launch {
+ writeMutex.withLock {
try {
- val raw = json.decodeFromString(message)
- val parsed = RawGPSData(
- sessionid = 0L,
- latitude = raw.latitude,
- longitude = raw.longitude,
- altitude = raw.altitude,
- speed = raw.speed,
- fixQuality = raw.satellites,
- // Stamped on receipt rather than the ESP32-reported timestamp string:
- // elapsed-time math (0-60, quarter mile, etc.) needs consistent
- // relative precision between samples, and the module's own timestamp
- // has no guaranteed sub-second resolution.
- timestamp = System.currentTimeMillis()
- )
- _gpsFlow.value = parsed // Use .value instead of .emit()
+ out.write(command.toByteArray(Charsets.US_ASCII))
+ out.flush()
} catch (e: Exception) {
- Log.e("ESPTcpClient", "JSON Parse Error: ${e.message} for input: $message")
+ Log.e("ESPTcpClient", "sendCommand failed: ${e.message}")
}
}
}
}
+ private suspend fun processChunk(buffer: ByteArray, length: Int) {
+ val message = buffer.decodeToString(0, length).trim()
+ if (message.isEmpty()) return
+
+ val ackedHz = parseRateAck(message)
+ if (ackedHz != null) {
+ _confirmedRateHz.value = ackedHz
+ return
+ }
+ if (message == "RATE_ERR") {
+ Log.w("ESPTcpClient", "GPS module rejected rate change")
+ return
+ }
+
+ withContext(Dispatchers.Default) {
+ try {
+ val raw = gpsJsonParser.decodeFromString(message)
+ _gpsFlow.value = raw.toEntity() // Use .value instead of .emit()
+ } catch (e: Exception) {
+ Log.e("ESPTcpClient", "JSON Parse Error: ${e.message} for input: $message")
+ }
+ }
+ }
+
fun disconnect() {
running.set(false)
runCatching { socket?.close() }
+ outputStream = null
_connectionStatus.value = false
}
@@ -127,77 +161,8 @@ class ESPTcpClient(
Log.e("ESPTcpClient", "Error closing socket: ${e.message}")
}
socket = null
+ outputStream = null
running.set(false)
_connectionStatus.value = false
}
-
- // --- Helper Classes ---
-
- @Serializable
- private data class RawGPSDataRaw(
- val latitude: Double,
- val longitude: Double,
- val altitude: Double = 0.0,
- val speed: Float = 0f,
- val satellites: Int = 0,
- val timestamp: String
- )
-
- class DelimitedInputStreamReader(
- private val input: InputStream,
- private val delimiter: ByteArray
- ) {
- private val buffer = ByteArrayOutputStream()
-
- fun read(target: ByteArray): Int {
- try {
- while (true) {
- val byte = input.read()
- if (byte == -1) return -1
-
- buffer.write(byte)
-
- if (endsWithDelimiter()) {
- val fullData = buffer.toByteArray()
- val length = fullData.size - delimiter.size
-
- // Ensure we don't overflow the target buffer
- val finalSize = if (length > target.size) target.size else length
- System.arraycopy(fullData, 0, target, 0, finalSize)
-
- buffer.reset()
- return finalSize
- }
-
- // Emergency flush if buffer gets too large (corrupt stream protection)
- if (buffer.size() > 2048) buffer.reset()
- }
- } catch (_: Exception) {
- return -1
- }
- }
-
- private fun endsWithDelimiter(): Boolean {
- val data = buffer.toByteArray()
- if (data.size < delimiter.size) return false
- for (i in delimiter.indices) {
- if (data[data.size - delimiter.size + i] != delimiter[i]) return false
- }
- return true
- }
- }
-
- class BufferPool(private val bufferSize: Int, poolSize: Int) {
- private val pool = ArrayDeque(poolSize).apply {
- repeat(poolSize) { add(ByteArray(bufferSize)) }
- }
-
- @Synchronized
- fun obtain(): ByteArray = pool.removeFirstOrNull() ?: ByteArray(bufferSize)
-
- @Synchronized
- fun recycle(buffer: ByteArray) {
- if (pool.size < 10) pool.addLast(buffer)
- }
- }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/GpsManager.kt b/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/GpsManager.kt
index f845c09..d1a2d3d 100644
--- a/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/GpsManager.kt
+++ b/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/GpsManager.kt
@@ -1,56 +1,89 @@
package com.example.trackpro.managerClasses.gpsDataManagers
+import com.example.trackpro.models.CommandableGpsProvider
+import com.example.trackpro.models.GpsProvider
+import com.example.trackpro.models.GpsProviderType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
class GpsManager(
- private val espProvider: ESPTcpClient,
+ private val wifiProvider: ESPTcpClient,
+ private val bluetoothProvider: BluetoothClassicClient,
private val phoneProvider: PhoneGpsProvider,
- private val useExternalGps: StateFlow
+ private val gpsSource: StateFlow,
+ private val selectedRateHz: StateFlow
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
- @OptIn(ExperimentalCoroutinesApi::class)
- val activeGpsFlow = useExternalGps.flatMapLatest { isExternal ->
- if (isExternal) espProvider.gpsFlow else phoneProvider.gpsFlow
+ private fun providerFor(source: GpsProviderType): GpsProvider = when (source) {
+ GpsProviderType.WIFI -> wifiProvider
+ GpsProviderType.BLUETOOTH -> bluetoothProvider
+ GpsProviderType.PHONE_GPS -> phoneProvider
}
+ private val allProviders: List = listOf(wifiProvider, bluetoothProvider, phoneProvider)
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val activeGpsFlow = gpsSource.flatMapLatest { providerFor(it).gpsFlow }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val connectionStatus = gpsSource.flatMapLatest { providerFor(it).connectionStatus }
+
+ // ESP32-confirmed rate for the active provider; null for phone GPS (not
+ // commandable) or before any RATE_OK reply has been seen yet.
@OptIn(ExperimentalCoroutinesApi::class)
- val connectionStatus = useExternalGps.flatMapLatest { isExternal ->
- if (isExternal) espProvider.connectionStatus else phoneProvider.connectionStatus
+ val confirmedRateHz = gpsSource.flatMapLatest { source ->
+ (providerFor(source) as? CommandableGpsProvider)?.confirmedRateHz
+ ?: MutableStateFlow(null)
+ }
+
+ fun sendCommandToActive(cmd: String) {
+ (providerFor(gpsSource.value) as? CommandableGpsProvider)?.sendCommand(cmd)
}
init {
- // React to toggle changes — stop old, start new
+ // React to source changes — stop everything else, start the selected one
scope.launch {
- useExternalGps.collect { isExternal ->
- if (isExternal) {
- phoneProvider.stop()
- espProvider.start()
- } else {
- espProvider.stop()
- phoneProvider.start()
- }
+ gpsSource.collect { selected ->
+ val active = providerFor(selected)
+ allProviders.filter { it !== active }.forEach { it.stop() }
+ active.start()
}
}
+
+ // Re-assert the desired rate whenever the active provider becomes
+ // connected — covers both a fresh connection and switching source onto
+ // an already-live provider. distinctUntilChanged() MUST precede
+ // filter{it}: when switching sources the flattened sequence is
+ // true(old)->false(new starting)->true(new connected); filtering first
+ // would hide the intervening false, so distinctUntilChanged would then
+ // see true,true back-to-back and swallow the second reconnect's re-send.
+ scope.launch {
+ connectionStatus
+ .distinctUntilChanged()
+ .filter { it }
+ .collect { sendCommandToActive("RATE:${selectedRateHz.value}\n") }
+ }
}
fun startActiveProvider() {
- if (useExternalGps.value) espProvider.start() else phoneProvider.start()
+ providerFor(gpsSource.value).start()
}
fun stopActiveProvider() {
- espProvider.stop()
- phoneProvider.stop()
+ allProviders.forEach { it.stop() }
}
fun cancel() {
scope.cancel()
}
-}
\ No newline at end of file
+}
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 b63c7d7..b460ca7 100644
--- a/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt
@@ -1,5 +1,6 @@
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
@@ -49,24 +50,37 @@ import com.example.trackpro.theme.Spacing
import com.example.trackpro.theme.TrackProType
import com.example.trackpro.managerClasses.JsonReader
import com.example.trackpro.managerClasses.utilities.UnitFormatter
+import com.example.trackpro.models.GpsProviderType
import kotlin.math.cos
import kotlin.math.sin
+@SuppressLint("MissingPermission")
@Composable
-fun ESPConnectionTestScreen() {
+fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) {
val context = LocalContext.current
val app = context.applicationContext as TrackProApp
// 1. Unified State Collection
- val isConnected by app.gpsManager.connectionStatus.collectAsState(initial = false)
- val gpsData by app.gpsManager.activeGpsFlow.collectAsState(initial = null)
- val useExternal by app.useExternalGps.collectAsState()
- val useMetric by app.useMetricUnits.collectAsState()
+ val isConnected by app.gpsManager.connectionStatus.collectAsState(initial = false)
+ val gpsData by app.gpsManager.activeGpsFlow.collectAsState(initial = null)
+ val gpsSource by app.gpsSource.collectAsState()
+ val selectedRateHz by app.selectedRateHz.collectAsState()
+ val confirmedRateHz by app.gpsManager.confirmedRateHz.collectAsState(initial = null)
+ val selectedBtDeviceMac by app.selectedBtDeviceMac.collectAsState()
+ val useTestServer by app.useTestServer.collectAsState()
+ val testServerAddress by app.testServerAddress.collectAsState()
+ val useMetric by app.useMetricUnits.collectAsState()
// 2. Configuration for display
val config = remember { JsonReader.loadConfig(context) }
- val ip = config.first
+ val ip = if (useTestServer && testServerAddress.isNotBlank()) testServerAddress else config.first
val port = config.second
+ val pairedDeviceLabel = remember(selectedBtDeviceMac) {
+ app.bluetoothClassicClient.getBondedDevices()
+ .find { it.address == selectedBtDeviceMac }
+ ?.let { it.name ?: it.address }
+ ?: "None selected"
+ }
// 3. Derived UI values
val speed = gpsData?.speed ?: 0f
@@ -81,14 +95,18 @@ fun ESPConnectionTestScreen() {
Column(modifier = Modifier.fillMaxSize()) {
AppTopBar(
- title = if (useExternal) "ESP32 Mode" else "Phone GPS Mode",
- accent = if (isConnected) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentCyan,
+ title = when (gpsSource) {
+ GpsProviderType.WIFI -> "ESP32 (WiFi) Mode"
+ GpsProviderType.BLUETOOTH -> "ESP32 (Bluetooth) Mode"
+ GpsProviderType.PHONE_GPS -> "Phone GPS Mode"
+ },
+ accent = if (isConnected) TrackProTheme.colors.accent else TrackProTheme.colors.textFaint,
trailing = {
Text(
- text = "Switch",
+ text = "Change",
style = TrackProType.label,
- color = TrackProTheme.colors.accentCyan,
- modifier = Modifier.clickable { app.useExternalGps.value = !useExternal }
+ color = TrackProTheme.colors.accent,
+ modifier = Modifier.clickable { onNavigateToSettings() }
)
}
)
@@ -132,7 +150,11 @@ fun ESPConnectionTestScreen() {
) {
StatCell(
label = "Source",
- value = if (useExternal) "ESP32" else "Internal",
+ value = when (gpsSource) {
+ GpsProviderType.WIFI -> "ESP32 (WiFi)"
+ GpsProviderType.BLUETOOTH -> "ESP32 (BT)"
+ GpsProviderType.PHONE_GPS -> "Internal"
+ },
size = StatCellSize.Small,
horizontalAlignment = Alignment.CenterHorizontally
)
@@ -140,7 +162,7 @@ fun ESPConnectionTestScreen() {
StatCell(
label = "Status",
value = if (isConnected) "Live" else "Offline",
- valueColor = if (isConnected) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentCyan,
+ valueColor = if (isConnected) TrackProTheme.colors.deltaGood else TrackProTheme.colors.textMuted,
size = StatCellSize.Small,
horizontalAlignment = Alignment.CenterHorizontally
)
@@ -148,7 +170,7 @@ fun ESPConnectionTestScreen() {
StatCell(
label = "Fix",
value = if (fix) "OK" else "Wait",
- valueColor = if (fix) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentAmber,
+ valueColor = if (fix) TrackProTheme.colors.deltaGood else TrackProTheme.colors.textMuted,
size = StatCellSize.Small,
horizontalAlignment = Alignment.CenterHorizontally
)
@@ -166,8 +188,15 @@ fun ESPConnectionTestScreen() {
.padding(horizontal = Spacing.lg, vertical = Spacing.md),
verticalArrangement = Arrangement.spacedBy(Spacing.md)
) {
- if (useExternal) {
- TelemetryRow("Remote IP", "$ip:$port", TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted)
+ when (gpsSource) {
+ GpsProviderType.WIFI -> TelemetryRow(
+ if (useTestServer) "Remote IP (Test)" else "Remote IP",
+ "$ip:$port",
+ if (useTestServer) TrackProTheme.colors.accent else TrackProTheme.colors.textPrimary,
+ TrackProTheme.colors.textMuted
+ )
+ GpsProviderType.BLUETOOTH -> TelemetryRow("Paired Device", pairedDeviceLabel, TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted)
+ GpsProviderType.PHONE_GPS -> {}
}
TelemetryRow("Latitude", gpsData?.latitude?.let { String.format("%.6f°", it) } ?: "—", TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted)
TelemetryRow("Longitude", gpsData?.longitude?.let { String.format("%.6f°", it) } ?: "—", TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted)
@@ -175,8 +204,12 @@ fun ESPConnectionTestScreen() {
TelemetryRow(
"Refresh",
- if (useExternal) "20-25 Hz" else "1-5 Hz",
- if (useExternal) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentAmber,
+ when {
+ gpsSource == GpsProviderType.PHONE_GPS -> "1-5 Hz"
+ confirmedRateHz != null -> "$confirmedRateHz Hz"
+ else -> "$selectedRateHz Hz (pending)"
+ },
+ TrackProTheme.colors.textPrimary,
TrackProTheme.colors.textMuted
)
}
@@ -194,7 +227,7 @@ fun ESPConnectionTestScreen() {
) {
Text(
text = gpsData?.toString() ?: "Awaiting data stream...",
- color = if (gpsData != null) TrackProTheme.colors.accentBlue else TrackProTheme.colors.textMuted,
+ color = if (gpsData != null) TrackProTheme.colors.accent else TrackProTheme.colors.textMuted,
fontSize = 10.sp,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
lineHeight = 16.sp
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 b68c345..74d1759 100644
--- a/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt
@@ -1,5 +1,7 @@
package com.example.trackpro.screens
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothDevice
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -11,6 +13,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -20,20 +24,28 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
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.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.Spacing
import com.example.trackpro.theme.TrackProType
@Composable
-fun SettingsScreen(onBack: () -> Unit) {
+fun SettingsScreen(onBack: () -> Unit, onRequestBluetoothPermission: () -> Unit) {
val context = LocalContext.current
val app = context.applicationContext as TrackProApp
- val useExternal by app.useExternalGps.collectAsState()
+ val gpsSource by app.gpsSource.collectAsState()
+ val selectedRateHz by app.selectedRateHz.collectAsState()
+ val confirmedRateHz by app.gpsManager.confirmedRateHz.collectAsState(initial = null)
+ val selectedBtDeviceMac by app.selectedBtDeviceMac.collectAsState()
+ val useTestServer by app.useTestServer.collectAsState()
+ val testServerAddress by app.testServerAddress.collectAsState()
val useDarkTheme by app.useDarkTheme.collectAsState()
val useMetric by app.useMetricUnits.collectAsState()
@@ -53,14 +65,39 @@ fun SettingsScreen(onBack: () -> Unit) {
// --- Section: Hardware & GPS ---
SectionLabel("Hardware & Sensors")
AppCard {
- SettingsToggleRow(
- label = "GPS Source",
- valueText = if (useExternal) "External ESP32 Module" else "Internal Phone GPS",
- valueColor = if (useExternal) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textMuted,
- buttonText = if (useExternal) "Use Phone" else "Use ESP32",
- isActive = useExternal,
- onClick = { app.useExternalGps.value = !useExternal }
- )
+ Column(verticalArrangement = Arrangement.spacedBy(Spacing.md)) {
+ GpsSourceRow(
+ selected = gpsSource,
+ onSelect = { source ->
+ app.setGpsSource(source)
+ if (source == GpsProviderType.BLUETOOTH) onRequestBluetoothPermission()
+ }
+ )
+
+ if (gpsSource == GpsProviderType.BLUETOOTH) {
+ BluetoothDeviceRow(
+ devices = app.bluetoothClassicClient.getBondedDevices(),
+ selectedMac = selectedBtDeviceMac,
+ hasPermission = app.bluetoothClassicClient.hasBluetoothPermission(),
+ onSelect = { device -> app.setSelectedBtDevice(device.address) }
+ )
+ }
+
+ if (gpsSource == GpsProviderType.WIFI) {
+ EspTargetRow(
+ useTestServer = useTestServer,
+ testServerAddress = testServerAddress,
+ onToggle = { app.setUseTestServer(it) },
+ onAddressChange = { app.setTestServerAddress(it) }
+ )
+ }
+
+ GpsRateRow(
+ selectedHz = selectedRateHz,
+ confirmedHz = confirmedRateHz,
+ onSelect = { hz -> app.setRateHz(hz) }
+ )
+ }
}
// --- Section: Appearance ---
@@ -69,7 +106,7 @@ fun SettingsScreen(onBack: () -> Unit) {
SettingsToggleRow(
label = "Theme",
valueText = if (useDarkTheme) "Dark" else "Light",
- valueColor = TrackProTheme.colors.accentCyan,
+ valueColor = TrackProTheme.colors.textMuted,
buttonText = if (useDarkTheme) "Use Light" else "Use Dark",
isActive = true,
onClick = { app.setDarkTheme(!useDarkTheme) }
@@ -82,7 +119,7 @@ fun SettingsScreen(onBack: () -> Unit) {
SettingsToggleRow(
label = "Speed & Distance",
valueText = if (useMetric) "Metric (km/h, km)" else "Imperial (mph, mi)",
- valueColor = TrackProTheme.colors.accentCyan,
+ valueColor = TrackProTheme.colors.textMuted,
buttonText = if (useMetric) "Use mph" else "Use km/h",
isActive = true,
onClick = { app.setMetricUnits(!useMetric) }
@@ -104,6 +141,140 @@ fun SettingsScreen(onBack: () -> Unit) {
}
}
+@Composable
+private fun GpsSourceRow(selected: GpsProviderType, onSelect: (GpsProviderType) -> Unit) {
+ Column {
+ Text("GPS SOURCE", style = TrackProType.label, color = TrackProTheme.colors.textPrimary)
+ Spacer(Modifier.height(6.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(Spacing.sm)
+ ) {
+ GpsProviderType.values().forEach { source ->
+ ToggleChip(
+ text = gpsSourceLabel(source),
+ selected = selected == source,
+ onClick = { onSelect(source) },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+}
+
+private fun gpsSourceLabel(source: GpsProviderType): String = when (source) {
+ GpsProviderType.WIFI -> "WiFi"
+ GpsProviderType.BLUETOOTH -> "Bluetooth"
+ GpsProviderType.PHONE_GPS -> "Phone"
+}
+
+@SuppressLint("MissingPermission")
+@Composable
+private fun BluetoothDeviceRow(
+ devices: List,
+ selectedMac: String?,
+ hasPermission: Boolean,
+ onSelect: (BluetoothDevice) -> Unit
+) {
+ val selectedLabel = devices.find { it.address == selectedMac }?.let { it.name ?: it.address }
+ ?: "Select a paired device"
+ AppDropdownField(
+ label = "Bluetooth Device",
+ items = devices,
+ selectedLabel = selectedLabel,
+ itemLabel = { it.name ?: it.address },
+ onSelect = onSelect,
+ emptyMessage = if (hasPermission) {
+ "No paired devices — pair the ESP32 in Android Bluetooth settings first"
+ } else {
+ "Bluetooth permission needed — tap WiFi then Bluetooth again to re-prompt"
+ }
+ )
+}
+
+@Composable
+private fun EspTargetRow(
+ useTestServer: Boolean,
+ testServerAddress: String,
+ onToggle: (Boolean) -> Unit,
+ onAddressChange: (String) -> Unit
+) {
+ Column {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text("ESP TARGET", style = TrackProType.label, color = TrackProTheme.colors.textPrimary)
+ Spacer(Modifier.height(2.dp))
+ Text(
+ text = if (useTestServer) "Test Simulator" else "Real Device (192.168.4.1)",
+ style = TrackProType.body,
+ color = TrackProTheme.colors.textMuted
+ )
+ }
+ ToggleChip(
+ text = if (useTestServer) "Use Real Device" else "Use Test Simulator",
+ selected = useTestServer,
+ onClick = { onToggle(!useTestServer) },
+ accent = TrackProTheme.colors.accent
+ )
+ }
+
+ if (useTestServer) {
+ Spacer(Modifier.height(Spacing.sm))
+ OutlinedTextField(
+ value = testServerAddress,
+ onValueChange = onAddressChange,
+ label = { Text("Simulator IP Address", color = TrackProTheme.colors.textMuted) },
+ placeholder = { Text("e.g. 192.168.1.50", color = TrackProTheme.colors.textFaint) },
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth(),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedTextColor = TrackProTheme.colors.textPrimary,
+ unfocusedTextColor = TrackProTheme.colors.textPrimary,
+ focusedBorderColor = TrackProTheme.colors.accent,
+ unfocusedBorderColor = TrackProTheme.colors.sectorLine
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun GpsRateRow(selectedHz: Int, confirmedHz: Int?, onSelect: (Int) -> Unit) {
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ Text("GPS RATE", style = TrackProType.label, color = TrackProTheme.colors.textPrimary)
+ if (confirmedHz != null) {
+ Text(
+ text = if (confirmedHz == selectedHz) "· confirmed" else "· device at ${confirmedHz}Hz",
+ style = TrackProType.body.copy(fontSize = 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
+ else TrackProTheme.colors.deltaBad
+ )
+ }
+ }
+ Spacer(Modifier.height(6.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(Spacing.sm)
+ ) {
+ listOf(5, 10, 20, 25).forEach { hz ->
+ ToggleChip(
+ text = "$hz Hz",
+ selected = selectedHz == hz,
+ onClick = { onSelect(hz) },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+}
+
/**
* A labeled setting with a value line on the left and a single toggle action on the
* right. The three toggles on this screen (GPS source, theme, units) all used to
@@ -132,7 +303,7 @@ private fun SettingsToggleRow(
text = buttonText,
selected = isActive,
onClick = onClick,
- accent = TrackProTheme.colors.accentCyan
+ accent = TrackProTheme.colors.accent
)
}
}
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 4069e0a..ae185a4 100644
--- a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt
@@ -115,7 +115,7 @@ fun TrackBuilderScreen(
Box(modifier = Modifier.fillMaxSize().background(TrackProTheme.colors.bgDeep)) {
Column(modifier = Modifier.fillMaxSize()) {
- AppTopBar(title = "Track Builder", accent = TrackProTheme.colors.accentAmber, onBack = onBack)
+ AppTopBar(title = "Track Builder", accent = TrackProTheme.colors.accent, onBack = onBack)
Column(modifier = Modifier.padding(Spacing.md)) {
TrackInfoCard(trackName, countryName, trackMode) { showInfoDialog = true }
@@ -301,7 +301,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.accentCyan)
+ Text("Mode: ${mode.uppercase()}", style = TrackProType.body.copy(fontSize = 12.sp), color = TrackProTheme.colors.accent)
}
PrimaryButton(
text = "Edit",
@@ -320,7 +320,7 @@ private fun MarkSectorButton(count: Int, enabled: Boolean, onClick: () -> Unit)
text = "Mark Sector ${count + 1}",
onClick = onClick,
enabled = enabled,
- accent = TrackProTheme.colors.accentBlue,
+ accent = TrackProTheme.colors.accent,
modifier = Modifier.fillMaxWidth().height(48.dp)
)
}
@@ -415,7 +415,7 @@ private fun ManualControls(onUndo: () -> Unit, onSave: () -> Unit, canSave: Bool
text = "Save Track",
onClick = onSave,
enabled = canSave,
- accent = TrackProTheme.colors.accentCyan,
+ accent = TrackProTheme.colors.accent,
modifier = Modifier.weight(1f).height(56.dp)
)
}
@@ -461,7 +461,7 @@ fun TrackInfoAlert(
},
confirmButton = {
TextButton(onClick = { onConfirm(name, country, mode) }) {
- Text("DONE", color = TrackProTheme.colors.accentCyan, fontWeight = FontWeight.Bold)
+ Text("DONE", color = TrackProTheme.colors.accent, fontWeight = FontWeight.Bold)
}
},
dismissButton = {
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 bdc36ba..eba2eb8 100644
--- a/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt
@@ -105,7 +105,7 @@ fun TrackView(database: ESPDatabase, trackId: Long) {
) {
Column(modifier = Modifier.fillMaxSize()) {
- AppTopBar(title = "Track Overview", accent = TrackProTheme.colors.accentBlue)
+ AppTopBar(title = "Track Overview", accent = TrackProTheme.colors.accent)
// ── Track info card ───────────────────────────────
Column(
@@ -190,7 +190,7 @@ fun TrackView(database: ESPDatabase, trackId: Long) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(
- color = TrackProTheme.colors.accentBlue,
+ color = TrackProTheme.colors.accent,
modifier = Modifier.size(32.dp),
strokeWidth = 2.dp
)
@@ -234,7 +234,7 @@ private fun SectorSlicerCard(
Text(
text = if (sectorCount > 0) "$sectorCount marked" else "None marked",
style = TrackProType.body.copy(fontSize = 11.sp),
- color = if (sectorCount > 0) TrackProTheme.colors.accentBlue else TrackProTheme.colors.textMuted
+ color = if (sectorCount > 0) TrackProTheme.colors.accent else TrackProTheme.colors.textMuted
)
}
@@ -247,7 +247,7 @@ private fun SectorSlicerCard(
text = "$n",
selected = sectorCount == n,
onClick = { onSlice(n) },
- accent = TrackProTheme.colors.accentBlue,
+ accent = TrackProTheme.colors.accent,
modifier = Modifier.weight(1f)
)
}
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 9b83fff..17f41ee 100644
--- a/app/src/main/java/com/example/trackpro/screens/TrackVehicleSelectorScreen.kt
+++ b/app/src/main/java/com/example/trackpro/screens/TrackVehicleSelectorScreen.kt
@@ -65,7 +65,7 @@ fun TrackVehicleSelectorScreen(
Column(modifier = Modifier.fillMaxSize()) {
AppTopBar(
title = "Session Setup",
- accent = TrackProTheme.colors.accentCyan,
+ accent = TrackProTheme.colors.accent,
onBack = { navController.popBackStack() }
)
@@ -111,7 +111,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.accentCyan)
+ Text("No vehicles found in garage", style = TrackProType.body.copy(fontSize = 12.sp), color = TrackProTheme.colors.accent)
}
}
@@ -124,7 +124,7 @@ fun TrackVehicleSelectorScreen(
text = "Start Time Attack",
onClick = { navController.navigate("timeattack/$selectedVehicleId/$selectedTrackId") },
enabled = canStart,
- accent = TrackProTheme.colors.accentCyan,
+ accent = TrackProTheme.colors.accent,
modifier = Modifier.fillMaxWidth().height(56.dp)
)
}
@@ -141,12 +141,12 @@ fun SelectionCard(
) {
AppCard(
modifier = Modifier.fillMaxWidth(),
- borderColor = if (isSet) TrackProTheme.colors.accentCyan.copy(alpha = 0.5f) else TrackProTheme.colors.sectorLine
+ borderColor = if (isSet) TrackProTheme.colors.accent.copy(alpha = 0.5f) else TrackProTheme.colors.sectorLine
) {
Text(
label.uppercase(),
style = TrackProType.label,
- color = if (isSet) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textMuted
+ color = if (isSet) TrackProTheme.colors.accent else TrackProTheme.colors.textMuted
)
Text(title, style = TrackProType.titleMedium, color = TrackProTheme.colors.textPrimary, modifier = Modifier.padding(vertical = 4.dp))
Spacer(modifier = Modifier.height(Spacing.sm))
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 56ef5e7..67eb35b 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
@@ -69,7 +69,7 @@ fun CarListScreen(navController: NavController, viewModel: VehicleFULLViewModel)
AppTopBar(
title = "My Vehicles",
- accent = TrackProTheme.colors.accentAmber,
+ accent = TrackProTheme.colors.accent,
trailing = {
Text(
text = "${vehicles.size} cars",
@@ -96,7 +96,7 @@ fun CarListScreen(navController: NavController, viewModel: VehicleFULLViewModel)
navController = navController,
bgCard = TrackProTheme.colors.bgCard,
bgElevated = TrackProTheme.colors.bgElevated,
- accentAmber = TrackProTheme.colors.accentAmber,
+ accent = TrackProTheme.colors.accent,
dangerColor = TrackProTheme.colors.danger,
textPrimary = TrackProTheme.colors.textPrimary,
textMuted = TrackProTheme.colors.textMuted,
@@ -121,7 +121,7 @@ fun VehicleCard(
navController: NavController,
bgCard: Color,
bgElevated: Color,
- accentAmber: Color,
+ accent: Color,
dangerColor: Color,
textPrimary: Color,
textMuted: Color,
@@ -168,7 +168,7 @@ fun VehicleCard(
modifier = Modifier
.fillMaxWidth()
.background(
- accentAmber.copy(alpha = 0.12f),
+ TrackProTheme.colors.bgElevated,
RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)
)
.padding(horizontal = Spacing.md, vertical = Spacing.sm)
@@ -184,13 +184,13 @@ fun VehicleCard(
) {
Box(
modifier = Modifier
- .background(accentAmber.copy(alpha = 0.15f), TrackProShapes.badge)
+ .background(bgCard, TrackProShapes.badge)
.padding(horizontal = 6.dp, vertical = 2.dp)
) {
Text(
text = vehicle.fuelType.uppercase(),
style = TrackProType.label.copy(fontSize = 9.sp),
- color = accentAmber
+ color = textMuted
)
}
Text(
@@ -251,15 +251,15 @@ fun VehicleCard(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
- StatCell(label = "Power", value = "${vehicle.horsepower}", unit = "hp", valueColor = accentAmber, horizontalAlignment = Alignment.CenterHorizontally)
+ StatCell(label = "Power", value = "${vehicle.horsepower}", unit = "hp", horizontalAlignment = Alignment.CenterHorizontally)
StatCellDivider()
- StatCell(label = "Torque", value = vehicle.torque?.toString() ?: "—", unit = "Nm", valueColor = accentAmber, horizontalAlignment = Alignment.CenterHorizontally)
+ StatCell(label = "Torque", value = vehicle.torque?.toString() ?: "—", unit = "Nm", horizontalAlignment = Alignment.CenterHorizontally)
StatCellDivider()
- StatCell(label = "Weight", value = "${vehicle.weight}", unit = "kg", valueColor = accentAmber, horizontalAlignment = Alignment.CenterHorizontally)
+ StatCell(label = "Weight", value = "${vehicle.weight}", unit = "kg", horizontalAlignment = Alignment.CenterHorizontally)
Text(
text = "View",
style = TrackProType.label,
- color = accentAmber.copy(alpha = 0.8f)
+ color = accent
)
}
}
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 8f0e8dd..3351027 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
@@ -66,7 +66,7 @@ fun DragTimesListView(
Column(modifier = Modifier.fillMaxSize()) {
AppTopBar(
title = "Drag Records",
- accent = TrackProTheme.colors.accentCyan,
+ accent = TrackProTheme.colors.accent,
trailing = {
Text(
"${dragSessions.size} sessions",
@@ -105,7 +105,7 @@ fun ExpandableSessionGroup(
navController: NavController
) {
ExpandableGroup(
- accent = TrackProTheme.colors.accentCyan,
+ accent = TrackProTheme.colors.accent,
header = {
Column(modifier = Modifier.weight(1f)) {
Text(groupTitle, style = TrackProType.titleMedium.copy(fontSize = 13.sp), color = TrackProTheme.colors.textPrimary)
@@ -132,7 +132,7 @@ fun ExpandableSessionGroup(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
- Modifier.size(6.dp).background(TrackProTheme.colors.accentCyan, RoundedCornerShape(100))
+ Modifier.size(6.dp).background(TrackProTheme.colors.accentMuted, RoundedCornerShape(100))
)
Spacer(Modifier.width(Spacing.sm))
Text(
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 c8d5ef8..4739d16 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
@@ -66,7 +66,7 @@ fun TimeAttackListViewScreen(
Column(modifier = Modifier.fillMaxSize()) {
AppTopBar(
title = "Track Records",
- accent = TrackProTheme.colors.accentCyan,
+ accent = TrackProTheme.colors.accent,
trailing = {
Text("${trackSessions.size} sessions", style = TrackProType.label, color = TrackProTheme.colors.textMuted)
}
@@ -107,7 +107,7 @@ fun ExpandableTrackGroup(
navController: NavController
) {
ExpandableGroup(
- accent = TrackProTheme.colors.accentCyan,
+ accent = TrackProTheme.colors.accent,
header = {
Column(modifier = Modifier.weight(1f)) {
Text(trackName, style = TrackProType.titleMedium.copy(fontSize = 14.sp), color = TrackProTheme.colors.textPrimary)
@@ -136,7 +136,7 @@ fun ExpandableTrackGroup(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// Vertical "Pillar" accent
- Box(modifier = Modifier.width(2.dp).height(24.dp).background(TrackProTheme.colors.accentCyan, RoundedCornerShape(2.dp)))
+ Box(modifier = Modifier.width(2.dp).height(24.dp).background(TrackProTheme.colors.accentMuted, RoundedCornerShape(2.dp)))
Spacer(Modifier.width(Spacing.sm))
Column {
Text(
@@ -152,7 +152,7 @@ fun ExpandableTrackGroup(
}
}
- Text("Telemetry", style = TrackProType.label, color = TrackProTheme.colors.accentCyan)
+ Text("Telemetry", style = TrackProType.label, 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 8293131..897238f 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
@@ -72,7 +72,7 @@ fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) {
AppTopBar(
title = "My Tracks",
- accent = TrackProTheme.colors.accentAmber,
+ accent = TrackProTheme.colors.accent,
trailing = {
Text(
text = "${tracks.size} tracks",
@@ -98,7 +98,7 @@ fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) {
useMetric = useMetric,
bgCard = TrackProTheme.colors.bgCard,
bgElevated = TrackProTheme.colors.bgElevated,
- accentAmber = TrackProTheme.colors.accentAmber,
+ accent = TrackProTheme.colors.accent,
dangerColor = TrackProTheme.colors.danger,
textPrimary = TrackProTheme.colors.textPrimary,
textMuted = TrackProTheme.colors.textMuted,
@@ -124,7 +124,7 @@ fun TrackCard(
useMetric: Boolean,
bgCard: Color,
bgElevated: Color,
- accentAmber: Color,
+ accent: Color,
dangerColor: Color,
textPrimary: Color,
textMuted: Color,
@@ -174,7 +174,7 @@ fun TrackCard(
modifier = Modifier
.fillMaxWidth()
.background(
- accentAmber.copy(alpha = 0.12f),
+ TrackProTheme.colors.bgElevated,
RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)
)
.padding(horizontal = Spacing.md, vertical = Spacing.sm)
@@ -190,13 +190,13 @@ fun TrackCard(
) {
Box(
modifier = Modifier
- .background(accentAmber.copy(alpha = 0.15f), TrackProShapes.badge)
+ .background(bgCard, TrackProShapes.badge)
.padding(horizontal = 6.dp, vertical = 2.dp)
) {
Text(
text = track.type.uppercase(),
style = TrackProType.label.copy(fontSize = 9.sp),
- color = accentAmber
+ color = textMuted
)
}
Text(
@@ -252,8 +252,7 @@ fun TrackCard(
StatCell(
label = "Length",
// totalLength is stored in km; formatDistance takes meters.
- value = track.totalLength?.let { UnitFormatter.formatDistance(it * 1000.0, useMetric) } ?: "?",
- valueColor = accentAmber
+ value = track.totalLength?.let { UnitFormatter.formatDistance(it * 1000.0, useMetric) } ?: "?"
)
StatCell(
label = "Lap Record",
@@ -263,7 +262,7 @@ fun TrackCard(
Text(
text = "View",
style = TrackProType.label,
- color = accentAmber.copy(alpha = 0.8f)
+ color = accent
)
}
}
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 281a5df..e1e350b 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
@@ -69,7 +69,7 @@ fun CarViewScreen(vehicleId: Long) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(
- color = TrackProTheme.colors.accentAmber,
+ color = TrackProTheme.colors.accent,
modifier = Modifier.size(36.dp),
strokeWidth = 2.dp
)
@@ -81,7 +81,7 @@ fun CarViewScreen(vehicleId: Long) {
val vehicle = vehicleInfo!!
Column(modifier = Modifier.fillMaxSize()) {
- AppTopBar(title = "Vehicle Profile", accent = TrackProTheme.colors.accentAmber)
+ AppTopBar(title = "Vehicle Profile", accent = TrackProTheme.colors.accent)
LazyColumn(modifier = Modifier.fillMaxSize()) {
@@ -117,9 +117,9 @@ fun CarViewScreen(vehicleId: Long) {
.padding(horizontal = Spacing.lg, vertical = Spacing.md),
horizontalArrangement = Arrangement.SpaceBetween
) {
- StatCell(label = "Power", value = "${vehicle.horsepower}", unit = "hp", valueColor = TrackProTheme.colors.accentAmber, size = StatCellSize.Large, horizontalAlignment = Alignment.CenterHorizontally)
- StatCell(label = "Torque", value = vehicle.torque?.toString() ?: "—", unit = "Nm", valueColor = TrackProTheme.colors.accentAmber, size = StatCellSize.Large, horizontalAlignment = Alignment.CenterHorizontally)
- StatCell(label = "Weight", value = "${vehicle.weight}", unit = "kg", valueColor = TrackProTheme.colors.accentAmber, size = StatCellSize.Large, horizontalAlignment = Alignment.CenterHorizontally)
+ StatCell(label = "Power", value = "${vehicle.horsepower}", unit = "hp", size = StatCellSize.Large, horizontalAlignment = Alignment.CenterHorizontally)
+ StatCell(label = "Torque", value = vehicle.torque?.toString() ?: "—", unit = "Nm", size = StatCellSize.Large, horizontalAlignment = Alignment.CenterHorizontally)
+ StatCell(label = "Weight", value = "${vehicle.weight}", unit = "kg", size = StatCellSize.Large, horizontalAlignment = Alignment.CenterHorizontally)
}
HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp)
}
@@ -137,7 +137,6 @@ fun CarViewScreen(vehicleId: Long) {
label = "Top Speed",
value = vehicle.topSpeed?.let { UnitFormatter.formatSpeed(it, useMetric) } ?: "—",
unit = UnitFormatter.speedUnitLabel(useMetric).lowercase(),
- valueColor = TrackProTheme.colors.accentAmber,
size = StatCellSize.Large,
horizontalAlignment = Alignment.CenterHorizontally
)
@@ -145,14 +144,12 @@ fun CarViewScreen(vehicleId: Long) {
label = "0–100",
value = vehicle.acceleration?.toString() ?: "—",
unit = "sec",
- valueColor = TrackProTheme.colors.accentAmber,
size = StatCellSize.Large,
horizontalAlignment = Alignment.CenterHorizontally
)
StatCell(
label = "Drivetrain",
value = vehicle.drivetrain,
- valueColor = TrackProTheme.colors.accentAmber,
size = StatCellSize.Large,
horizontalAlignment = Alignment.CenterHorizontally
)
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 5c51967..957eeca 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
@@ -24,7 +24,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -210,7 +209,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
.fillMaxSize()
.background(TrackProTheme.colors.bgDeep)
) {
- AppTopBar(title = "Session Overview", accent = TrackProTheme.colors.accentCyan, onBack = onBack)
+ AppTopBar(title = "Session Overview", accent = TrackProTheme.colors.accent, onBack = onBack)
// ── Compact stats panel ───────────────────────────────
Column(
@@ -298,7 +297,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
Box(
modifier = Modifier
.background(
- if (active) TrackProTheme.colors.accentCyan else TrackProTheme.colors.sectorLine,
+ if (active) TrackProTheme.colors.accent else TrackProTheme.colors.sectorLine,
RoundedCornerShape(3.dp)
)
.clickable { xAxisInMeters = isMeters }
@@ -307,7 +306,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
Text(
text = label,
style = TrackProType.body.copy(fontSize = 9.sp),
- color = if (active) Color.Black else TrackProTheme.colors.textMuted
+ color = if (active) TrackProTheme.colors.onAccent else TrackProTheme.colors.textMuted
)
}
}
@@ -317,7 +316,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
Box(
modifier = Modifier
.background(
- if (active) TrackProTheme.colors.accentAmber else TrackProTheme.colors.sectorLine,
+ if (active) TrackProTheme.colors.accent else TrackProTheme.colors.sectorLine,
RoundedCornerShape(3.dp)
)
.clickable { showMap = isMap }
@@ -326,7 +325,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) {
Text(
text = label,
style = TrackProType.body.copy(fontSize = 9.sp),
- color = if (active) Color.Black else TrackProTheme.colors.textMuted
+ 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 c9e85a1..ad2355f 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
@@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.toColorInt
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import androidx.room.Room
@@ -181,7 +182,7 @@ fun LapDetailScreen(
) {
AppTopBar(
title = "Lap ${lap.lapnumber} · ${lap.laptime}",
- accent = TrackProTheme.colors.accentBlue,
+ accent = TrackProTheme.colors.accent,
onBack = { navController.popBackStack() },
trailing = {
Text(
@@ -209,7 +210,7 @@ fun LapDetailScreen(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(
- if (selected) TrackProTheme.colors.accentBlue
+ if (selected) TrackProTheme.colors.accent
else Color.Transparent
)
.clickable { heatmapMode = mode }
@@ -219,7 +220,7 @@ fun LapDetailScreen(
Text(
mode.label,
style = TrackProType.label,
- color = if (selected) Color.Black else TrackProTheme.colors.textMuted
+ color = if (selected) TrackProTheme.colors.onAccent else TrackProTheme.colors.textMuted
)
}
}
@@ -313,14 +314,14 @@ fun LapDetailScreen(
// Stats toggle
ActionButton(
label = if (showStatsPanel) "Hide Stats" else "Stats",
- color = TrackProTheme.colors.accentBlue,
+ color = TrackProTheme.colors.accent,
modifier = Modifier.weight(1f)
) { showStatsPanel = !showStatsPanel }
// Compare toggle
ActionButton(
label = if (compareLap != null) "Comparing" else "Compare",
- color = if (compareLap != null) COMPARE_COLOR else TrackProTheme.colors.accentAmber,
+ color = if (compareLap != null) COMPARE_COLOR else TrackProTheme.colors.accent,
modifier = Modifier.weight(1f)
) { showLapPicker = true }
}
@@ -349,11 +350,13 @@ fun LapDetailScreen(
// ── Heatmap Map View ───────────────────────────────────────
-// Dedicated (non-theme) colors: these draw MapLibre GPS traces from plain Kotlin
-// functions, which run outside any @Composable context and so can't read
-// TrackProTheme.colors. Chosen to read clearly against a dark map tile.
-private val PRIMARY_COLOR = Color(0xFF7B84D6)
-private val COMPARE_COLOR = Color(0xFF4FB6C9)
+// These draw MapLibre GPS traces from plain Kotlin functions, which run outside any
+// @Composable context and so can't read TrackProTheme.colors. Sourced from
+// DataVizColors so the two series stay in step with the rest of the palette - this is
+// the one sanctioned place in the app with two distinguishable hues, because telling
+// two overlaid lap traces apart is a real data problem.
+private val PRIMARY_COLOR = Color(DataVizColors.seriesPrimary.toColorInt())
+private val COMPARE_COLOR = Color(DataVizColors.seriesCompare.toColorInt())
@Composable
fun LapHeatmapMapView(
@@ -526,7 +529,7 @@ private fun drawEndpointDot(style: Style, pt: LapInfoData, id: String, color: St
setProperties(
PropertyFactory.circleColor(color),
PropertyFactory.circleRadius(5f),
- PropertyFactory.circleStrokeColor("#0E1117"),
+ PropertyFactory.circleStrokeColor(DataVizColors.darkOutline),
PropertyFactory.circleStrokeWidth(1.5f)
)
})
@@ -750,7 +753,7 @@ private fun LapPickerSheet(
.background(
when {
isSelected -> COMPARE_COLOR.copy(alpha = 0.15f)
- isBest -> TrackProTheme.colors.accentBlue.copy(alpha = 0.05f)
+ isBest -> TrackProTheme.colors.accent.copy(alpha = 0.05f)
else -> Color.Transparent
}
)
@@ -758,7 +761,7 @@ private fun LapPickerSheet(
width = 1.dp,
color = when {
isSelected -> COMPARE_COLOR.copy(alpha = 0.5f)
- isBest -> TrackProTheme.colors.accentBlue.copy(alpha = 0.2f)
+ isBest -> TrackProTheme.colors.accent.copy(alpha = 0.2f)
else -> Color.Transparent
},
shape = RoundedCornerShape(8.dp)
@@ -775,15 +778,15 @@ private fun LapPickerSheet(
Text(
String.format("%02d", lap.lapnumber),
style = TrackProType.statValue.copy(fontSize = 16.sp),
- color = if (isBest) TrackProTheme.colors.accentBlue else TrackProTheme.colors.textPrimary
+ color = if (isBest) TrackProTheme.colors.accent else TrackProTheme.colors.textPrimary
)
if (isBest) {
Box(
Modifier
- .background(TrackProTheme.colors.accentBlue.copy(alpha = 0.15f), TrackProShapes.badge)
+ .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.accentBlue)
+ Text("Best", style = TrackProType.label.copy(fontSize = 7.sp), color = TrackProTheme.colors.accent)
}
}
}
@@ -817,7 +820,7 @@ private fun LoadingView() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(
- color = TrackProTheme.colors.accentBlue,
+ color = TrackProTheme.colors.accent,
modifier = Modifier.size(32.dp),
strokeWidth = 2.dp
)
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 8a0e4d6..0b2d796 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
@@ -125,7 +125,7 @@ fun TimeAttackListItemScreen(
if (isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- CircularProgressIndicator(color = TrackProTheme.colors.accentBlue,
+ CircularProgressIndicator(color = TrackProTheme.colors.accent,
modifier = Modifier.size(36.dp), strokeWidth = 2.dp)
Spacer(Modifier.height(12.dp))
Text("Loading session", style = TrackProType.label, color = TrackProTheme.colors.textFaint)
@@ -175,7 +175,7 @@ fun TimeAttackListItemScreen(
val trendColor = when {
trend.contains("IMPROVING") -> TrackProTheme.colors.deltaGood
trend.contains("FADING") -> TrackProTheme.colors.deltaBad
- else -> TrackProTheme.colors.accentAmber
+ else -> TrackProTheme.colors.textMuted
}
LazyColumn(
@@ -186,7 +186,7 @@ fun TimeAttackListItemScreen(
item {
AppTopBar(
title = "Session Detail",
- accent = TrackProTheme.colors.accentBlue,
+ accent = TrackProTheme.colors.accent,
trailing = {
Text("${lapTimes.size} laps", style = TrackProType.label, color = TrackProTheme.colors.textMuted)
}
@@ -342,7 +342,6 @@ fun TimeAttackListItemScreen(
bgElevated = TrackProTheme.colors.bgElevated,
goodColor = TrackProTheme.colors.deltaGood,
badColor = TrackProTheme.colors.deltaBad,
- accentAmber = TrackProTheme.colors.accentAmber,
textPrimary = TrackProTheme.colors.textPrimary,
textMuted = TrackProTheme.colors.textMuted,
sectorLine = TrackProTheme.colors.sectorLine
@@ -369,7 +368,6 @@ private fun LapRow(
bgElevated: Color,
goodColor: Color,
badColor: Color,
- accentAmber: Color,
textPrimary: Color,
textMuted: Color,
sectorLine: Color
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 c647cc2..c44ed94 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
@@ -36,7 +36,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -155,7 +154,7 @@ fun DragRaceScreen(
// 1. TOP STATUS BAR
AppTopBar(
title = "Drag Mode",
- accent = TrackProTheme.colors.accentCyan,
+ accent = TrackProTheme.colors.accent,
trailing = {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -165,7 +164,7 @@ fun DragRaceScreen(
Text(
elapsedTime,
style = TrackProType.statValue.copy(fontSize = 13.sp),
- color = TrackProTheme.colors.accentCyan
+ color = TrackProTheme.colors.accent
)
}
Text(
@@ -280,7 +279,7 @@ fun DragRaceScreen(
.fillMaxWidth()
.padding(horizontal = Spacing.md),
padding = Spacing.lg,
- borderColor = if (isSessionActive) TrackProTheme.colors.accentCyan.copy(alpha = 0.5f)
+ borderColor = if (isSessionActive) TrackProTheme.colors.accent.copy(alpha = 0.5f)
else TrackProTheme.colors.sectorLine
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -297,7 +296,7 @@ fun DragRaceScreen(
Text(
gpsData?.speed?.let { UnitFormatter.formatSpeed(it, useMetric) } ?: "0",
style = TrackProType.displayNumeric,
- color = if (isSessionActive) TrackProTheme.colors.accentCyan
+ color = if (isSessionActive) TrackProTheme.colors.accent
else TrackProTheme.colors.textPrimary
)
Text(
@@ -505,8 +504,8 @@ fun DragRaceScreen(
}
},
enabled = selectedVehicle != null,
- accent = if (isSessionActive) TrackProTheme.colors.bgElevated else TrackProTheme.colors.accentCyan,
- contentColor = if (isSessionActive) TrackProTheme.colors.accentCyan else null,
+ 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)
)
}
@@ -518,15 +517,16 @@ fun DragMetricCard(
metric: DragMetricDisplay,
modifier: Modifier = Modifier
) {
+ // "Achieved" is signalled by a tinted border plus a small accent dot, and the value
+ // brightening from dim to full. An earlier version also washed the whole tile in
+ // accent and colored the label - four cues on eleven tiles at once, which is what
+ // made this grid read as noisy.
AppCard(
modifier = modifier,
- borderColor = if (metric.achieved) TrackProTheme.colors.accentCyan.copy(alpha = 0.6f) else TrackProTheme.colors.sectorLine
+ borderColor = if (metric.achieved) TrackProTheme.colors.accent.copy(alpha = 0.45f)
+ else TrackProTheme.colors.sectorLine
) {
- Column(
- modifier = Modifier.background(
- if (metric.achieved) TrackProTheme.colors.accentCyan.copy(alpha = 0.08f) else Color.Transparent
- )
- ) {
+ Column {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -535,13 +535,13 @@ fun DragMetricCard(
Text(
metric.label.uppercase(),
style = TrackProType.label,
- color = if (metric.achieved) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textFaint
+ color = if (metric.achieved) TrackProTheme.colors.textMuted else TrackProTheme.colors.textFaint
)
if (metric.achieved) {
Box(
Modifier
- .size(6.dp)
- .background(TrackProTheme.colors.accentCyan, CircleShape)
+ .size(5.dp)
+ .background(TrackProTheme.colors.accent, CircleShape)
)
}
}
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 7788764..9d6dc62 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
@@ -216,7 +216,7 @@ fun TimeAttackPortraitLayout(
) {
val deltaColor = if (delta <= 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad
val eventName = if (timingMode is TimingMode.Circuit) "LAP" else "RUN"
- val modeColor = if (timingMode is TimingMode.Circuit) TrackProTheme.colors.accentCyan else TrackProTheme.colors.accentAmber
+ val modeColor = TrackProTheme.colors.accent
val modeLabel = if (timingMode is TimingMode.Circuit) "CIRCUIT" else "SPRINT"
Box(
@@ -359,7 +359,7 @@ fun TimeAttackLandscapeLayout(
) {
val deltaColor = if (delta <= 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad
val eventName = if (timingMode is TimingMode.Circuit) "LAP" else "RUN"
- val modeColor = if (timingMode is TimingMode.Circuit) TrackProTheme.colors.accentCyan else TrackProTheme.colors.accentAmber
+ val modeColor = TrackProTheme.colors.accent
val modeLabel = if (timingMode is TimingMode.Circuit) "CIRCUIT" else "SPRINT"
Row(
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 88b6450..2c9a2c4 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
@@ -83,7 +83,7 @@ fun CarCreationScreen(
) {
Column(modifier = Modifier.fillMaxSize()) {
- AppTopBar(title = "Vehicle Setup", accent = TrackProTheme.colors.accentAmber)
+ AppTopBar(title = "Vehicle Setup", accent = TrackProTheme.colors.accent)
Column(
modifier = Modifier
@@ -95,28 +95,28 @@ fun CarCreationScreen(
AppCard(modifier = Modifier.fillMaxWidth(), padding = 20.dp) {
SectionLabel("Basic Info (Required)", modifier = Modifier.padding(vertical = Spacing.sm))
- CustomTextField("Manufacturer", manufacturer, leadingIcon = Icons.Default.Business, accent = TrackProTheme.colors.accentAmber) { manufacturer = it }
- CustomTextField("Model", model, leadingIcon = Icons.Default.DirectionsCar, accent = TrackProTheme.colors.accentAmber) { model = it }
- CustomTextField("Year", year, leadingIcon = Icons.Default.Event, accent = TrackProTheme.colors.accentAmber) { year = it }
+ 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, accent = TrackProTheme.colors.accentAmber) { horsepower = it }
- CustomTextField("Torque (Nm)", torque, true, Icons.Default.Settings, accent = TrackProTheme.colors.accentAmber) { torque = it }
- CustomTextField("Weight (kg)", weight, true, Icons.Default.FitnessCenter, accent = TrackProTheme.colors.accentAmber) { weight = it }
- CustomTextField("Top Speed (${UnitFormatter.speedUnitLabel(useMetric)})", topSpeed, true, Icons.Default.Speed, accent = TrackProTheme.colors.accentAmber) { topSpeed = it }
+ 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, accent = TrackProTheme.colors.accentAmber
+ acceleration, true, Icons.Default.Timer
) { acceleration = it }
- CustomTextField("Fuel Capacity (L)", fuelCapacity, true, Icons.Default.LocalGasStation, accent = TrackProTheme.colors.accentAmber) { fuelCapacity = 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 }, accent = TrackProTheme.colors.accentAmber)
- AppDropdownField("Drivetrain", jsonOptions.drivetrains, selectedDrivetrain, { it }, { selectedDrivetrain = it }, accent = TrackProTheme.colors.accentAmber)
- AppDropdownField("Fuel Type", jsonOptions.fuelTypes, selectedFuelType, { it }, { selectedFuelType = it }, accent = TrackProTheme.colors.accentAmber)
- AppDropdownField("Tire Type", jsonOptions.tireTypes, selectedTireType, { it }, { selectedTireType = it }, accent = TrackProTheme.colors.accentAmber)
- AppDropdownField("Transmission", jsonOptions.transmissions, selectedTransmission, { it }, { selectedTransmission = it }, accent = TrackProTheme.colors.accentAmber)
- AppDropdownField("Suspension", jsonOptions.suspensionTypes, selectedSuspensionType, { it }, { selectedSuspensionType = it }, accent = TrackProTheme.colors.accentAmber)
+ 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))
diff --git a/app/src/main/java/com/example/trackpro/theme/TrackProColors.kt b/app/src/main/java/com/example/trackpro/theme/TrackProColors.kt
index 939bc8a..2e1a351 100644
--- a/app/src/main/java/com/example/trackpro/theme/TrackProColors.kt
+++ b/app/src/main/java/com/example/trackpro/theme/TrackProColors.kt
@@ -6,9 +6,11 @@ data class TrackProColorScheme(
val bgDeep: Color,
val bgCard: Color,
val bgElevated: Color,
- val accentCyan: Color,
- val accentBlue: Color,
- val accentAmber: Color,
+ val accent: Color,
+ /** Foreground for content sitting on top of an [accent] fill (selected chips, CTAs). */
+ val onAccent: Color,
+ /** The quieter accent - structural marks that repeat, where [accent] would shout. */
+ val accentMuted: Color,
val textPrimary: Color,
val textMuted: Color,
val textFaint: Color,
@@ -19,39 +21,73 @@ data class TrackProColorScheme(
)
/**
- * Accents are desaturated on purpose and are meant to stay small (icons, hairline
- * underlines, badges, dots) — never a full-bleed fill. bgDeep/bgCard/bgElevated carry the
- * visual weight instead. deltaGood/deltaBad/danger stay closer to full saturation since
- * they're functional signal colors (faster/slower, destructive), not decoration.
+ * Built on the supplied 5-color palette:
+ *
+ * #16262E darkest navy -> bgDeep
+ * #2E4756 dark slate -> bgElevated / chart grid
+ * #3C7A89 teal -> accentMuted, map + chart lines
+ * #9FA2B2 cool grey -> textMuted
+ * #FEEA00 yellow -> accent
+ *
+ * Five colors can't cover a whole UI's tonal needs, so bgCard, sectorLine and the text
+ * tiers are interpolated *within* that ramp rather than invented - bgCard sits between
+ * #16262E and #2E4756, sectorLine just above #2E4756, and textPrimary is #9FA2B2 lifted
+ * toward white so it clears 11:1 on the deep background.
+ *
+ * Color hierarchy, in order of loudness:
+ *
+ * 1. [accent] (#FEEA00) - active / selected / primary action only. It hits 12.6:1 on the
+ * background, so it carries enormous weight; used sparingly it reads as a signal lamp,
+ * used everywhere it reads as a highlighter.
+ * 2. [accentMuted] (#3C7A89) - the structural marks that repeat per row or per card:
+ * icon chips, left accent bars, map and chart lines. Same family, a fraction of the
+ * impact. This is a loudness hierarchy, not the per-section color rotation an earlier
+ * revision had - don't reintroduce that.
+ * 3. [deltaGood]/[deltaBad] - faster/slower, and nothing else.
+ *
+ * The palette has no green or red, but a lap timer's delta has to be readable at a glance
+ * at speed and green/red is the one convention drivers already know. They're tuned to sit
+ * with the palette (the green leans teal, the red leans toward the yellow's warmth) rather
+ * than taken off the shelf.
+ *
+ * Neutral, always: surfaces, top bars, card headers, form fields, and static spec values.
*/
val DarkTrackProColors = TrackProColorScheme(
- bgDeep = Color(0xFF0B0D11),
- bgCard = Color(0xFF12151A),
- bgElevated = Color(0xFF1A1E26),
- accentCyan = Color(0xFF4FB6C9),
- accentBlue = Color(0xFF7B84D6),
- accentAmber = Color(0xFFD6A44A),
- textPrimary = Color(0xFFF0F2F6),
- textMuted = Color(0xFF8B93A3),
- textFaint = Color(0xFF545C6B),
- deltaGood = Color(0xFF3ECC7A),
- deltaBad = Color(0xFFEF5B54),
- sectorLine = Color(0xFF232830),
- danger = Color(0xFFEF5B54)
+ bgDeep = Color(0xFF16262E),
+ bgCard = Color(0xFF1D3038),
+ bgElevated = Color(0xFF2E4756),
+ accent = Color(0xFFFEEA00),
+ onAccent = Color(0xFF16262E),
+ accentMuted = Color(0xFF3C7A89),
+ textPrimary = Color(0xFFDCDEE6),
+ textMuted = Color(0xFF9FA2B2),
+ textFaint = Color(0xFF6E7385),
+ deltaGood = Color(0xFF2FBF71),
+ deltaBad = Color(0xFFEF4E3A),
+ sectorLine = Color(0xFF3A5563),
+ danger = Color(0xFFEF4E3A)
)
+/**
+ * The supplied palette is inherently dark, so light mode is a derived inversion: the
+ * darkest navy becomes the text color and the teal becomes the accent.
+ *
+ * Yellow is deliberately *not* the light-mode accent. [accent] is used as text as well as
+ * fill (link hints, the top-bar dot), and #FEEA00 on white is ~1.2:1 - illegible. Teal
+ * darkened to #2C6B7A clears 5.9:1 and keeps the palette's family.
+ */
val LightTrackProColors = TrackProColorScheme(
- bgDeep = Color(0xFFF0F2F5),
+ bgDeep = Color(0xFFEDEFF2),
bgCard = Color(0xFFFFFFFF),
- bgElevated = Color(0xFFEDF0F4),
- accentCyan = Color(0xFF3E93A6),
- accentBlue = Color(0xFF6169C7),
- accentAmber = Color(0xFFB9812E),
- textPrimary = Color(0xFF12151A),
- textMuted = Color(0xFF667085),
- textFaint = Color(0xFF98A2B3),
- deltaGood = Color(0xFF1C9B57),
- deltaBad = Color(0xFFD3453D),
- sectorLine = Color(0xFFE2E5EA),
- danger = Color(0xFFD3453D)
-)
\ No newline at end of file
+ bgElevated = Color(0xFFE3E7EB),
+ accent = Color(0xFF2C6B7A),
+ onAccent = Color(0xFFFFFFFF),
+ accentMuted = Color(0xFF3C7A89),
+ textPrimary = Color(0xFF16262E),
+ textMuted = Color(0xFF4F5F6C),
+ textFaint = Color(0xFF8A95A1),
+ deltaGood = Color(0xFF158A4E),
+ deltaBad = Color(0xFFC93A28),
+ sectorLine = Color(0xFFC8D0D8),
+ danger = Color(0xFFC93A28)
+)