From b5d25350d0c962aef8113ab576da56eebd18a42b Mon Sep 17 00:00:00 2001 From: "meszaros.martin986@gmail.com" Date: Tue, 4 Aug 2026 08:37:43 +0200 Subject: [PATCH 1/2] Expanded GPS source options and introduced Bluetooth support This commit significantly expands the GPS connectivity options by introducing support for Bluetooth Classic (SPP) alongside the existing WiFi (ESP32) and internal phone GPS providers. It also adds a testing mode for simulator redirection and implements GPS refresh rate controls. **Key Changes:** * **`GpsManager.kt`:** * Refactored to handle multiple `GpsProvider` types: `WIFI`, `BLUETOOTH`, and `PHONE_GPS`. * Implemented logic to switch between providers, ensuring only the selected source is active. * Added support for sending commands (like rate changes) to the active commandable provider. * Introduced `confirmedRateHz` to track hardware-confirmed refresh rates. * **`MainActivity.kt`:** * Initialized `BluetoothClassicClient` and implemented Bluetooth permission handling. * Added persistent storage for GPS source, preferred refresh rate, and selected Bluetooth device using `SharedPreferences`. * Implemented a "Test Simulator" redirect for WiFi connections, allowing the app to connect to a custom IP address for development/testing. * **`SettingsScreen.kt`:** * Revamped the "Hardware & Sensors" section with new UI for selecting GPS sources. * Added a Bluetooth device picker (dropdown) for paired devices. * Added a "Test Simulator" toggle and IP address field for WiFi configuration. * Introduced a GPS rate selector (5Hz to 25Hz) with a "confirmed" status indicator. * **`ESPTcpClient.kt`:** * Implemented `CommandableGpsProvider` to support sending data to the ESP32. * Added `updateTarget` to allow runtime redirection of the TCP connection without app restarts. * Updated JSON processing to handle rate acknowledgments (`RATE_OK`) and errors from the firmware. * **`ESPConnectionTestScreen.kt`:** * Updated the UI to display the current GPS source (WiFi, Bluetooth, or Phone). * Added display for the confirmed refresh rate and the name of the paired Bluetooth device. * **Models & Data:** * **`RawGPSData.kt`:** Added an optional `valid` boolean field. * **`ESPDatabase.kt`:** Bumped database version to `4`. * **`AndroidManifest.xml`:** Added necessary permissions for Bluetooth and `BLUETOOTH_CONNECT`. * **`BluetoothClassicClient.kt` (New - implied by manager changes):** * Introduced to handle SPP communication with external Bluetooth GPS modules. --- app/src/main/AndroidManifest.xml | 4 + .../java/com/example/trackpro/MainActivity.kt | 104 +++++++++- .../trackpro/dataClasses/RawGPSData.kt | 3 +- .../trackpro/managerClasses/ESPDatabase.kt | 2 +- .../gpsDataManagers/ESPTcpClient.kt | 175 +++++++--------- .../gpsDataManagers/GpsManager.kt | 73 +++++-- .../screens/ESPConnectionTestScreen.kt | 61 ++++-- .../trackpro/screens/SettingsScreen.kt | 188 +++++++++++++++++- 8 files changed, 454 insertions(+), 156 deletions(-) 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..6f0e1f7 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 -> 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/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..e6eebe1 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", + title = when (gpsSource) { + GpsProviderType.WIFI -> "ESP32 (WiFi) Mode" + GpsProviderType.BLUETOOTH -> "ESP32 (Bluetooth) Mode" + GpsProviderType.PHONE_GPS -> "Phone GPS Mode" + }, accent = if (isConnected) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentCyan, trailing = { Text( - text = "Switch", + text = "Change", style = TrackProType.label, color = TrackProTheme.colors.accentCyan, - modifier = Modifier.clickable { app.useExternalGps.value = !useExternal } + 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 ) @@ -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.accentAmber 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)" + }, + if (gpsSource != GpsProviderType.PHONE_GPS) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentAmber, TrackProTheme.colors.textMuted ) } 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..6ebd8df 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 --- @@ -104,6 +141,137 @@ 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 = if (useTestServer) TrackProTheme.colors.accentAmber else TrackProTheme.colors.accentCyan + ) + } + ToggleChip( + text = if (useTestServer) "Use Real Device" else "Use Test Simulator", + selected = useTestServer, + onClick = { onToggle(!useTestServer) }, + accent = TrackProTheme.colors.accentAmber + ) + } + + 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.accentAmber, + 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), + color = if (confirmedHz == selectedHz) TrackProTheme.colors.accentCyan else TrackProTheme.colors.accentAmber + ) + } + } + 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 From 2fdb029dd7c26ae85b0ad713f9fd9d1cb2171702 Mon Sep 17 00:00:00 2001 From: "meszaros.martin986@gmail.com" Date: Tue, 4 Aug 2026 14:31:47 +0200 Subject: [PATCH 2/2] Refactored UI theme and consolidated color system This commit implements a major refactor of the application's color palette, moving from multiple section-specific accents (`accentCyan`, `accentBlue`, `accentAmber`) to a unified `accent` and `accentMuted` system. The new theme is built on a specific 5-color palette, improving visual hierarchy and reducing UI noise across all screens. **Key Changes:** * **`TrackProColors.kt` & `TrackProTheme.kt`:** * Removed `accentCyan`, `accentBlue`, and `accentAmber`. * Introduced `accent` (Yellow `#FEEA00` in dark mode), `onAccent` for high-contrast foregrounds, and `accentMuted` (Teal `#3C7A89`) for structural elements. * Updated `DarkTrackProColors` and `LightTrackProColors` with the new interpolated color ramp. * Mapped Material 3 color slots (`primary`, `secondary`, `tertiary`) to the single unified accent. * **Global UI Components:** * Updated `AppTopBar`, `CircularProgressIndicator`, `PrimaryButton`, and `CustomTextField` across all screens to use the unified `accent`. * Refined `AppCard` styling to use subtler border and background treatments for achieved states. * **Screens (`TrackScreen.kt`, `DragScreen.kt`, `TimeAttackScreen.kt`, etc.):** * Replaced section-specific colors with `accent` or `accentMuted`. * Standardized "View" buttons and loading indicators to use the primary accent. * Updated `StatCell` to rely more on standard text colors rather than bright accents for better readability. * **Settings & Connectivity:** * Updated `SettingsScreen.kt` to use `textMuted` for toggle values, reserving color for functional warnings (e.g., GPS rate mismatches). * Updated `ESPConnectionTestScreen.kt` to use `deltaGood` for "Live" status and primary accent for data streams. * **Data Visualization (`LapDetailScreen.kt`):** * Sourced `PRIMARY_COLOR` and `COMPARE_COLOR` for map traces from `DataVizColors` to ensure consistency with charts while maintaining distinct hues for overlaid data. --- .../java/com/example/trackpro/MainActivity.kt | 20 ++-- .../trackpro/extrasForUI/CustomTextField.kt | 2 +- .../trackpro/extrasForUI/DropDownMenuField.kt | 6 +- .../trackpro/extrasForUI/TrackProTheme.kt | 29 ++--- .../screens/ESPConnectionTestScreen.kt | 14 +-- .../trackpro/screens/SettingsScreen.kt | 17 +-- .../trackpro/screens/TrackBuilderScreen.kt | 10 +- .../example/trackpro/screens/TrackScreen.kt | 8 +- .../screens/TrackVehicleSelectorScreen.kt | 10 +- .../screens/listViewScreens/CarListView.kt | 20 ++-- .../screens/listViewScreens/DragTimesList.kt | 6 +- .../listViewScreens/TimeAttackListView.kt | 8 +- .../screens/listViewScreens/TrackListView.kt | 17 ++- .../listItems/CarViewScreen.kt | 13 +-- .../listItems/DragScreenListItem.kt | 11 +- .../listItems/LapDetailScreen.kt | 37 ++++--- .../listItems/TimeAttackListItem.kt | 8 +- .../screens/telemetricScreens/DragScreen.kt | 32 +++--- .../telemetricScreens/TimeAttackScreen.kt | 4 +- .../vehicleScreens/CarCreatorScreen.kt | 32 +++--- .../example/trackpro/theme/TrackProColors.kt | 102 ++++++++++++------ 21 files changed, 222 insertions(+), 184 deletions(-) diff --git a/app/src/main/java/com/example/trackpro/MainActivity.kt b/app/src/main/java/com/example/trackpro/MainActivity.kt index 6f0e1f7..b70d201 100644 --- a/app/src/main/java/com/example/trackpro/MainActivity.kt +++ b/app/src/main/java/com/example/trackpro/MainActivity.kt @@ -431,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 { @@ -455,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() } } ) } @@ -476,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() } } ) } @@ -557,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( @@ -590,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 ) @@ -599,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 ) @@ -620,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 ) @@ -630,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/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/screens/ESPConnectionTestScreen.kt b/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt index e6eebe1..b460ca7 100644 --- a/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt @@ -100,12 +100,12 @@ fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) { GpsProviderType.BLUETOOTH -> "ESP32 (Bluetooth) Mode" GpsProviderType.PHONE_GPS -> "Phone GPS Mode" }, - accent = if (isConnected) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentCyan, + accent = if (isConnected) TrackProTheme.colors.accent else TrackProTheme.colors.textFaint, trailing = { Text( text = "Change", style = TrackProType.label, - color = TrackProTheme.colors.accentCyan, + color = TrackProTheme.colors.accent, modifier = Modifier.clickable { onNavigateToSettings() } ) } @@ -162,7 +162,7 @@ fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) { 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 ) @@ -170,7 +170,7 @@ fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) { 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 ) @@ -192,7 +192,7 @@ fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) { GpsProviderType.WIFI -> TelemetryRow( if (useTestServer) "Remote IP (Test)" else "Remote IP", "$ip:$port", - if (useTestServer) TrackProTheme.colors.accentAmber else TrackProTheme.colors.textPrimary, + if (useTestServer) TrackProTheme.colors.accent else TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted ) GpsProviderType.BLUETOOTH -> TelemetryRow("Paired Device", pairedDeviceLabel, TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted) @@ -209,7 +209,7 @@ fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) { confirmedRateHz != null -> "$confirmedRateHz Hz" else -> "$selectedRateHz Hz (pending)" }, - if (gpsSource != GpsProviderType.PHONE_GPS) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentAmber, + TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted ) } @@ -227,7 +227,7 @@ fun ESPConnectionTestScreen(onNavigateToSettings: () -> Unit) { ) { 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 6ebd8df..74d1759 100644 --- a/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt @@ -106,7 +106,7 @@ fun SettingsScreen(onBack: () -> Unit, onRequestBluetoothPermission: () -> 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) } @@ -119,7 +119,7 @@ fun SettingsScreen(onBack: () -> Unit, onRequestBluetoothPermission: () -> 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) } @@ -211,14 +211,14 @@ private fun EspTargetRow( Text( text = if (useTestServer) "Test Simulator" else "Real Device (192.168.4.1)", style = TrackProType.body, - color = if (useTestServer) TrackProTheme.colors.accentAmber else TrackProTheme.colors.accentCyan + color = TrackProTheme.colors.textMuted ) } ToggleChip( text = if (useTestServer) "Use Real Device" else "Use Test Simulator", selected = useTestServer, onClick = { onToggle(!useTestServer) }, - accent = TrackProTheme.colors.accentAmber + accent = TrackProTheme.colors.accent ) } @@ -234,7 +234,7 @@ private fun EspTargetRow( colors = OutlinedTextFieldDefaults.colors( focusedTextColor = TrackProTheme.colors.textPrimary, unfocusedTextColor = TrackProTheme.colors.textPrimary, - focusedBorderColor = TrackProTheme.colors.accentAmber, + focusedBorderColor = TrackProTheme.colors.accent, unfocusedBorderColor = TrackProTheme.colors.sectorLine ) ) @@ -251,7 +251,10 @@ private fun GpsRateRow(selectedHz: Int, confirmedHz: Int?, onSelect: (Int) -> Un Text( text = if (confirmedHz == selectedHz) "· confirmed" else "· device at ${confirmedHz}Hz", style = TrackProType.body.copy(fontSize = 10.sp), - color = if (confirmedHz == selectedHz) TrackProTheme.colors.accentCyan else TrackProTheme.colors.accentAmber + // 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 ) } } @@ -300,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) +)