From d80b026953d334770270d659fafb5cb4b4888134 Mon Sep 17 00:00:00 2001 From: "meszaros.martin986@gmail.com" Date: Thu, 30 Jul 2026 18:37:20 +0200 Subject: [PATCH 1/5] Summarize the changes into a concise commit message following the requested format. --- ### Refine GPS data handling, lap time precision, and MapLibre visualizations This commit improves the accuracy and reliability of GPS data processing, refines lap time formatting, and enhances track/session map visualizations. **Key Changes:** * **GPS Data Providers (`PhoneGpsProvider.kt`, `ESPTcpClient.kt`):** * Updated `timestamp` assignment to use `System.currentTimeMillis()` upon receipt. This ensures consistent relative precision for elapsed-time calculations (e.g., 0-60 mph) compared to relying on variable-resolution hardware timestamps. * Updated default `ip_address` in `config.json` to `192.168.4.1`. * **Time Utilities (`TimeUtils.kt`):** * Updated lap time formatting and parsing to use hundredths of a second (`MM:SS.hh`) instead of milliseconds (`MM:SS.mmm`) to maintain consistency across the UI and storage. * **Map Visualizations (`TrackScreen.kt`, `TrackBuilderScreen.kt`):** * **`TrackScreen.kt`:** Refactored static track rendering to handle single-point tracks gracefully (centering the camera without drawing degenerate lines). * **`TrackBuilderScreen.kt`:** Added camera following logic to the track builder; the map now automatically eases to the latest recorded point at a fixed zoom level. * Standardized MapLibre imports and utilized `CameraUpdateFactory` for smoother transitions. * **Drag Session UI (`DragScreenListItem.kt`):** * Introduced a `smoothSpeeds` utility using a centered moving average to reduce GPS jitter in speed graphs and "Max Accel" calculations. * Applied smoothed speed data to both the `GraphScreen` entries and the `DragSessionMapView`. * Updated acceleration logic to calculate change in speed over change in time using smoothed values. --- .../gpsDataManagers/ESPTcpClient.kt | 7 +- .../gpsDataManagers/PhoneGpsProvider.kt | 5 +- .../managerClasses/utilities/TimeUtils.kt | 11 +- .../trackpro/screens/TrackBuilderScreen.kt | 10 ++ .../example/trackpro/screens/TrackScreen.kt | 100 ++++++++++-------- .../listItems/DragScreenListItem.kt | 30 +++++- app/src/main/res/raw/config.json | 2 +- 7 files changed, 110 insertions(+), 55 deletions(-) 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 e1c4b96..90b354a 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,6 @@ package com.example.trackpro.managerClasses.gpsDataManagers import android.util.Log import com.example.trackpro.dataClasses.RawGPSData -import com.example.trackpro.managerClasses.calculationClasses.convertToUnixTimestamp import com.example.trackpro.models.GpsProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -100,7 +99,11 @@ class ESPTcpClient( altitude = raw.altitude, speed = raw.speed, fixQuality = raw.satellites, - timestamp = convertToUnixTimestamp(raw.timestamp) + // 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() } catch (e: Exception) { diff --git a/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/PhoneGpsProvider.kt b/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/PhoneGpsProvider.kt index 202cbb5..a36586d 100644 --- a/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/PhoneGpsProvider.kt +++ b/app/src/main/java/com/example/trackpro/managerClasses/gpsDataManagers/PhoneGpsProvider.kt @@ -38,7 +38,10 @@ class PhoneGpsProvider( altitude = loc.altitude, speed = loc.speed * 3.6f, // CRITICAL: Convert m/s to km/h fixQuality = if (loc.accuracy < 10) 3 else 1, - timestamp = loc.time + // Stamped on receipt rather than using loc.time: elapsed-time math (0-60, + // quarter mile, etc.) needs consistent relative precision between samples, + // and this is the same clock the live view already uses for that math. + timestamp = System.currentTimeMillis() ) } } diff --git a/app/src/main/java/com/example/trackpro/managerClasses/utilities/TimeUtils.kt b/app/src/main/java/com/example/trackpro/managerClasses/utilities/TimeUtils.kt index da6726f..738d308 100644 --- a/app/src/main/java/com/example/trackpro/managerClasses/utilities/TimeUtils.kt +++ b/app/src/main/java/com/example/trackpro/managerClasses/utilities/TimeUtils.kt @@ -1,17 +1,20 @@ package com.example.trackpro.managerClasses.utilities +// Lap times throughout the app are stored/displayed as "MM:SS.hh" (hundredths of a +// second, 2 digits) - see TimingManager.formatTime / TimeAttackViewModel.formatLapTime. +// These must stay exact inverses of that format and of each other. fun String.toLapTimeMillis(): Long { val parts = this.split(":", ".", limit = 3) val minutes = parts.getOrNull(0)?.toLongOrNull() ?: 0L val seconds = parts.getOrNull(1)?.toLongOrNull() ?: 0L - val millis = parts.getOrNull(2)?.toLongOrNull() ?: 0L - return minutes * 60_000 + seconds * 1_000 + millis + val hundredths = parts.getOrNull(2)?.toLongOrNull() ?: 0L + return minutes * 60_000 + seconds * 1_000 + hundredths * 10 } fun Long.toLapTimeString(): String { val abs = if (this < 0) -this else this val minutes = abs / 60_000 val seconds = (abs % 60_000) / 1_000 - val millis = abs % 1_000 - return String.format("%02d:%02d.%03d", minutes, seconds, millis) + val hundredths = (abs % 1_000) / 10 + return String.format("%02d:%02d.%02d", minutes, seconds, hundredths) } 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 981eea3..657dee2 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt @@ -57,6 +57,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.maplibre.android.annotations.MarkerOptions import org.maplibre.android.annotations.PolylineOptions +import org.maplibre.android.camera.CameraUpdateFactory import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.MapLibreMap import org.maplibre.android.maps.MapView @@ -248,6 +249,15 @@ fun MapLibreBuilderView( .title("S${(sectorPoint.sectorIndex ?: 0) + 1}") ) } + + // Follow the most recent point - without this the camera stays wherever + // it started (a default world view), so a live recording never visibly + // moves even though points are being collected correctly. + val last = points.last() + map.easeCamera( + CameraUpdateFactory.newLatLngZoom(LatLng(last.latitude, last.longitude), 17.0), + 500 + ) } } } 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 facff0e..3f693b3 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt @@ -39,6 +39,16 @@ import com.example.trackpro.extrasForUI.TrackProTheme import com.example.trackpro.managerClasses.ESPDatabase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.maplibre.android.MapLibre +import org.maplibre.android.camera.CameraUpdateFactory +import org.maplibre.android.geometry.LatLng +import org.maplibre.android.geometry.LatLngBounds +import org.maplibre.android.maps.MapView +import org.maplibre.android.style.layers.CircleLayer +import org.maplibre.android.style.layers.LineLayer +import org.maplibre.android.style.layers.Property +import org.maplibre.android.style.layers.PropertyFactory +import org.maplibre.android.style.sources.GeoJsonSource @Composable @@ -195,13 +205,13 @@ fun TrackStaticMapView( trackType: String, modifier: Modifier = Modifier ) { - var mapViewRef by remember { mutableStateOf(null) } + var mapViewRef by remember { mutableStateOf(null) } val trackDrawn = remember { mutableStateOf(false) } AndroidView( factory = { ctx -> - org.maplibre.android.MapLibre.getInstance(ctx) - org.maplibre.android.maps.MapView(ctx).also { mv -> + MapLibre.getInstance(ctx) + MapView(ctx).also { mv -> mapViewRef = mv mv.onCreate(null) mv.getMapAsync { map -> @@ -211,56 +221,60 @@ fun TrackStaticMapView( map.uiSettings.isLogoEnabled = false map.uiSettings.isAttributionEnabled = false - val coords = trackParts.joinToString(",") { - "[${it.longitude},${it.latitude}]" - } - - // Only close the loop if it's a Circuit - val finalCoordinates = if (trackType == "Circuit") { - val first = trackParts.first() - "$coords,[${first.longitude},${first.latitude}]" - } else { - coords // Keep it as an open line for Sprints - } + // A single (or empty) point can't form a line or a real bounding + // box - draw only the start marker and center on it directly, + // rather than feeding MapLibre a degenerate LineString/bounds. + if (trackParts.size >= 2) { + val coords = trackParts.joinToString(",") { + "[${it.longitude},${it.latitude}]" + } - val geojson = """{"type":"Feature","geometry":{"type":"LineString","coordinates":[$finalCoordinates]},"properties":{}}""" - val src = org.maplibre.android.style.sources.GeoJsonSource("track-static-src", geojson) - style.addSource(src) - style.addLayer( - org.maplibre.android.style.layers.LineLayer("track-static-layer", "track-static-src").apply { - setProperties( - org.maplibre.android.style.layers.PropertyFactory.lineColor("#00C853"), - org.maplibre.android.style.layers.PropertyFactory.lineWidth(3f), - org.maplibre.android.style.layers.PropertyFactory.lineCap( - org.maplibre.android.style.layers.Property.LINE_CAP_ROUND - ) - ) + // Only close the loop if it's a Circuit + val finalCoordinates = if (trackType == "Circuit") { + val first = trackParts.first() + "$coords,[${first.longitude},${first.latitude}]" + } else { + coords // Keep it as an open line for Sprints } - ) - // Start/finish marker - val startGeojson = """{"type":"Feature","geometry":{"type":"Point","coordinates":[$finalCoordinates}]},"properties":{}}""" + val geojson = """{"type":"Feature","geometry":{"type":"LineString","coordinates":[$finalCoordinates]},"properties":{}}""" + style.addSource(GeoJsonSource("track-static-src", geojson)) + style.addLayer( + LineLayer("track-static-layer", "track-static-src").apply { + setProperties( + PropertyFactory.lineColor("#00C853"), + PropertyFactory.lineWidth(3f), + PropertyFactory.lineCap(Property.LINE_CAP_ROUND) + ) + } + ) + } - style.addSource(org.maplibre.android.style.sources.GeoJsonSource("start-src", startGeojson)) + // Start/finish marker - a single point at the actual start coordinate + val startPoint = trackParts.first() + val startGeojson = """{"type":"Feature","geometry":{"type":"Point","coordinates":[${startPoint.longitude},${startPoint.latitude}]},"properties":{}}""" + style.addSource(GeoJsonSource("start-src", startGeojson)) style.addLayer( - org.maplibre.android.style.layers.CircleLayer("start-layer", "start-src").apply { + CircleLayer("start-layer", "start-src").apply { setProperties( - org.maplibre.android.style.layers.PropertyFactory.circleColor("#E8001C"), - org.maplibre.android.style.layers.PropertyFactory.circleRadius(8f), - org.maplibre.android.style.layers.PropertyFactory.circleStrokeColor("#FFFFFF"), - org.maplibre.android.style.layers.PropertyFactory.circleStrokeWidth(2f) + PropertyFactory.circleColor("#E8001C"), + PropertyFactory.circleRadius(8f), + PropertyFactory.circleStrokeColor("#FFFFFF"), + PropertyFactory.circleStrokeWidth(2f) ) } ) - val bounds = org.maplibre.android.geometry.LatLngBounds.Builder() - .includes(trackParts.map { - org.maplibre.android.geometry.LatLng(it.latitude, it.longitude) - }) - .build() - map.easeCamera( - org.maplibre.android.camera.CameraUpdateFactory.newLatLngBounds(bounds, 64), 800 - ) + if (trackParts.size >= 2) { + val bounds = LatLngBounds.Builder() + .includes(trackParts.map { LatLng(it.latitude, it.longitude) }) + .build() + map.easeCamera(CameraUpdateFactory.newLatLngBounds(bounds, 64), 800) + } else { + map.moveCamera( + CameraUpdateFactory.newLatLngZoom(LatLng(startPoint.latitude, startPoint.longitude), 16.0) + ) + } trackDrawn.value = true } } 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 b417015..a307c04 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 @@ -77,6 +77,19 @@ private fun haversineMeters(lat1: Double, lon1: Double, lat2: Double, lon2: Doub return R * 2 * atan2(sqrt(a), sqrt(1 - a)) } +// Centered moving average over GPS speed samples, tolerant of null readings. Smooths out +// per-sample GPS jitter for display/differentiation without touching the raw stored data +// (0-60/quarter-mile threshold detection still uses the unsmoothed values for accuracy). +private fun smoothSpeeds(data: List, windowSize: Int = 5): List { + val half = windowSize / 2 + return data.indices.map { i -> + val lo = (i - half).coerceAtLeast(0) + val hi = (i + half).coerceAtMost(data.lastIndex) + val window = (lo..hi).mapNotNull { data[it].speed } + if (window.isEmpty()) null else window.average().toFloat() + } +} + @Composable fun GraphScreen(onBack: () -> Unit, sessionId: Long) { @@ -85,6 +98,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { val useMetric by app.useMetricUnits.collectAsState() val database = remember { ESPDatabase.getInstance(context) } var coordinates by remember { mutableStateOf(emptyList()) } + var mapGpsData by remember { mutableStateOf(emptyList()) } val dragTimeClass = remember { DragTimeCalculation(sessionId, database) } var totalDist by remember { mutableDoubleStateOf(-1.0) } var showMap by remember { mutableStateOf(false) } @@ -114,6 +128,11 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { val data = database.rawGPSDataDao().getGPSDataBySession(sessionId) if (data.isEmpty()) return@withContext + // Raw per-sample GPS speed is noisy enough that both the chart and the + // differentiated MAX ACCEL stat look jagged even for a genuinely smooth run. + // Smooth it with a small centered moving average before using it for either. + val smoothedSpeeds = smoothSpeeds(data) + // Build cumulative distance array for X axis val cumulativeDist = DoubleArray(data.size) for (i in 1 until data.size) { @@ -127,7 +146,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { dataPointsMeters.clear() dataPointsSeconds.clear() data.forEachIndexed { i, d -> - d.speed?.let { + smoothedSpeeds[i]?.let { dataPointsMeters.add(Entry(cumulativeDist[i].toFloat(), it)) dataPointsSeconds.add(Entry(((d.timestamp - t0) / 1000f), it)) } @@ -143,7 +162,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { var maxAccel = -1.0 for (i in 1 until data.size) { - val dSpeed = (data[i].speed ?: continue) - (data[i - 1].speed ?: continue) + val dSpeed = (smoothedSpeeds[i] ?: continue) - (smoothedSpeeds[i - 1] ?: continue) val dTime = (data[i].timestamp - data[i - 1].timestamp) / 1000.0 if (dTime > 0) { val accel = dSpeed / dTime @@ -164,8 +183,11 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { val lastAlt = data.lastOrNull { it.altitude != null }?.altitude val net = if (firstAlt != null && lastAlt != null) lastAlt - firstAlt else 0.0 + val smoothedMapData = data.mapIndexed { i, d -> d.copy(speed = smoothedSpeeds[i]) } + withContext(Dispatchers.Main) { coordinates = data + mapGpsData = smoothedMapData metrics = calculatedMetrics totalDist = totalDistValue maxSpeed = maxSpeedValue @@ -338,8 +360,8 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { .background(TrackProTheme.colors.bgCard) ) { if (showMap) { - if (coordinates.isNotEmpty()) { - DragSessionMapView(gpsData = coordinates, modifier = Modifier.fillMaxSize()) + if (mapGpsData.isNotEmpty()) { + DragSessionMapView(gpsData = mapGpsData, modifier = Modifier.fillMaxSize()) } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( diff --git a/app/src/main/res/raw/config.json b/app/src/main/res/raw/config.json index 58e455c..c8d14a3 100644 --- a/app/src/main/res/raw/config.json +++ b/app/src/main/res/raw/config.json @@ -1,4 +1,4 @@ { - "ip_address": "10.0.2.2", + "ip_address": "192.168.4.1", "port": 4210 } \ No newline at end of file From 3cbe294c4ff0c770a9eed6dde8a895ed03088fdd Mon Sep 17 00:00:00 2001 From: "meszaros.martin986@gmail.com" Date: Fri, 31 Jul 2026 08:29:05 +0200 Subject: [PATCH 2/5] Implement live delta tracking, automated sector slicing, and dynamic unit support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a continuously updating live delta for TimeAttack sessions, adds the ability to automatically slice tracks into sectors, and implements a centralized `UnitFormatter` to support both Metric and Imperial units across the application. **Key Changes:** * **Live Delta Timing:** * **`CircuitTiming.kt`:** Now records a distance-based trace of the best lap. It calculates a real-time delta by comparing the current lap's elapsed time against the best lap at the same distance point. * **`TimeAttackScreen.kt` & `TimeAttackViewModel.kt`:** Exposed and integrated `liveDelta` into the UI with a "● LIVE" indicator. * **Track Sector Management:** * **`TrackGeometry.kt`:** Added `autoSliceSectors` to divide a track into equal distance intervals (2 to 6 sectors). * **`TrackScreen.kt`:** Introduced a `SectorSlicerCard` allowing users to re-slice or clear sectors on existing tracks. * **`TrackCoordinatesDataDAO.kt`:** Added `updateTrackCoordinates` for bulk-updating sector markers. * **Dynamic Units & Formatting:** * Integrated `UnitFormatter` across `DragScreen`, `TimeAttackListItem`, `CarViewScreen`, and `TrackListView` to dynamically toggle between KM/H (Metric) and MPH (Imperial) based on user preference. * Updated `CarCreatorScreen` to handle unit-aware input for top speed and acceleration. * **Map Enhancements:** * **`TrackScreen.kt`:** Refactored `TrackStaticMapView` to support dynamic re-renders when sector markers change and added visual markers for sectors (Amber circles). * **Bug Fixes & Refactoring:** * **`TrackBuilderScreen.kt`:** Corrected track length calculation to store values in kilometers to maintain consistency with database defaults. * **`ESPConnectionTestScreen.kt`:** Applied unit conversion to the speedometer display. --- .../trackpro/dao/TrackCoordinatesDataDAO.kt | 6 + .../timeAttackManagers/CircuitTiming.kt | 73 ++++- .../timeAttackManagers/TrackGeometry.kt | 44 +++ .../screens/ESPConnectionTestScreen.kt | 6 +- .../trackpro/screens/TrackBuilderScreen.kt | 9 +- .../example/trackpro/screens/TrackScreen.kt | 278 ++++++++++++++---- .../screens/listViewScreens/TrackListView.kt | 9 +- .../listItems/CarViewScreen.kt | 9 +- .../listItems/TimeAttackListItem.kt | 14 +- .../screens/telemetricScreens/DragScreen.kt | 12 +- .../telemetricScreens/TimeAttackScreen.kt | 82 ++++-- .../vehicleScreens/CarCreatorScreen.kt | 16 +- .../viewModels/TimeAttackViewModel.kt | 7 + 13 files changed, 461 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/com/example/trackpro/dao/TrackCoordinatesDataDAO.kt b/app/src/main/java/com/example/trackpro/dao/TrackCoordinatesDataDAO.kt index 55683f6..9717087 100644 --- a/app/src/main/java/com/example/trackpro/dao/TrackCoordinatesDataDAO.kt +++ b/app/src/main/java/com/example/trackpro/dao/TrackCoordinatesDataDAO.kt @@ -3,6 +3,7 @@ package com.example.trackpro.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import androidx.room.Update import com.example.trackpro.dataClasses.TrackCoordinatesData import kotlinx.coroutines.flow.Flow @@ -20,6 +21,11 @@ interface TrackCoordinatesDataDAO { @Insert suspend fun insertTrack(data: List) + // Bulk-updates existing points in place (matched by primary key) - used to (re)apply + // sector markers to an already-saved track without re-recording it. + @Update + suspend fun updateTrackCoordinates(data: List) + // IF the user whats to recreate the track //OR // IF the user filters the coordinates (if the full track is complete) diff --git a/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/CircuitTiming.kt b/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/CircuitTiming.kt index 2fe5b3f..b667161 100644 --- a/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/CircuitTiming.kt +++ b/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/CircuitTiming.kt @@ -2,6 +2,7 @@ package com.example.trackpro.managerClasses.timeAttackManagers import android.os.SystemClock import com.example.trackpro.dataClasses.TrackCoordinatesData +import com.example.trackpro.managerClasses.utilities.haversineDistance import kotlinx.coroutines.channels.Channel import com.example.trackpro.dataClasses.RawGPSData import kotlinx.coroutines.flow.MutableStateFlow @@ -23,18 +24,36 @@ class CircuitTimingManager( private var currentSectorIndex = 0 private val bestSectorMs = mutableMapOf() + // Live delta vs. the session's best lap, updated continuously by distance travelled + // in the current lap rather than only once per lap/sector boundary. currentLapTrace + // records (distanceMeters, elapsedMs) as the lap is driven; if the lap turns out to be + // a new best on completion, it's promoted to bestLapTrace for future comparisons. + private var currentLapDistanceMeters = 0.0 + private val currentLapTrace = mutableListOf>() + private var bestLapTrace: List> = emptyList() + val lapCompletedChannel = Channel(Channel.UNLIMITED) val sectorCompletedChannel = Channel(Channel.UNLIMITED) private val _currentLapSplits = MutableStateFlow>(emptyList()) val currentLapSplits: StateFlow> = _currentLapSplits.asStateFlow() + private val _liveDelta = MutableStateFlow(null) + val liveDelta: StateFlow = _liveDelta.asStateFlow() + override fun handleGpsUpdate( prev: RawGPSData?, current: RawGPSData ) { val now = SystemClock.elapsedRealtime() prev?.let { prevData -> + if (hasStarted) { + currentLapDistanceMeters += haversineDistance( + prevData.latitude, prevData.longitude, + current.latitude, current.longitude + ) + } + val finishCrossing = TrackGeometry.checkLineCrossing(prevData, current, finishLine) var finishHandled = false @@ -46,15 +65,24 @@ class CircuitTimingManager( lapStartTime = now lastSplitTime = now currentSectorIndex = 0 + currentLapDistanceMeters = 0.0 + currentLapTrace.clear() _currentLapSplits.value = emptyList() + _liveDelta.value = null } else { val lapMs = now - lapStartTime - updateTimes(lapMs) + val isNewBest = updateTimes(lapMs) + if (isNewBest) { + bestLapTrace = currentLapTrace.toList() + } lastCrossTime = now lapStartTime = now lastSplitTime = now currentSectorIndex = 0 + currentLapDistanceMeters = 0.0 + currentLapTrace.clear() _currentLapSplits.value = emptyList() + _liveDelta.value = null _eventCount.value += 1 lapCompletedChannel.trySend(lapMs) } @@ -80,18 +108,50 @@ class CircuitTimingManager( currentSectorIndex += 1 } } + + // Record this point into the current lap's trace and compute the continuous + // delta against the best lap's trace at the same distance-into-lap. + if (!finishHandled && hasStarted) { + val elapsedMs = now - lapStartTime + currentLapTrace.add(currentLapDistanceMeters to elapsedMs) + _liveDelta.value = interpolatedElapsedAtDistance(currentLapDistanceMeters) + ?.let { bestElapsedMs -> (elapsedMs - bestElapsedMs) / 1000.0 } + } } _currentTime.value = formatTime(now - lapStartTime) } - private fun updateTimes(lapMs: Long) { + /** Linearly interpolates the best lap's elapsed time at the given distance into the lap. */ + private fun interpolatedElapsedAtDistance(distance: Double): Long? { + if (bestLapTrace.isEmpty()) return null + val first = bestLapTrace.first() + val last = bestLapTrace.last() + if (distance <= first.first) return first.second + if (distance >= last.first) return last.second + + for (i in 1 until bestLapTrace.size) { + val (d0, t0) = bestLapTrace[i - 1] + val (d1, t1) = bestLapTrace[i] + if (distance <= d1) { + if (d1 <= d0) return t0 + val frac = (distance - d0) / (d1 - d0) + return (t0 + frac * (t1 - t0)).toLong() + } + } + return last.second + } + + /** Returns true if this lap became the new session-best. */ + private fun updateTimes(lapMs: Long): Boolean { val seconds = lapMs / 1000.0 _delta.value = if (bestLapSeconds.isFinite()) seconds - bestLapSeconds else 0.0 - if (seconds < bestLapSeconds) { + val isNewBest = seconds < bestLapSeconds + if (isNewBest) { bestLapSeconds = seconds _bestTime.value = formatTime(lapMs) } _lastTime.value = formatTime(lapMs) + return isNewBest } @@ -102,7 +162,11 @@ class CircuitTimingManager( hasStarted = false currentSectorIndex = 0 bestSectorMs.clear() + currentLapDistanceMeters = 0.0 + currentLapTrace.clear() + bestLapTrace = emptyList() _currentLapSplits.value = emptyList() + _liveDelta.value = null _stintStart.value = lapStartTime _eventCount.value = 0 } @@ -111,7 +175,10 @@ class CircuitTimingManager( lapStartTime = SystemClock.elapsedRealtime() lastSplitTime = lapStartTime currentSectorIndex = 0 + currentLapDistanceMeters = 0.0 + currentLapTrace.clear() _currentLapSplits.value = emptyList() + _liveDelta.value = null _currentTime.value = formatTime(0) } } diff --git a/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/TrackGeometry.kt b/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/TrackGeometry.kt index fbdae49..7cb3635 100644 --- a/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/TrackGeometry.kt +++ b/app/src/main/java/com/example/trackpro/managerClasses/timeAttackManagers/TrackGeometry.kt @@ -3,6 +3,8 @@ package com.example.trackpro.managerClasses.timeAttackManagers import android.util.Log import com.example.trackpro.dataClasses.RawGPSData import com.example.trackpro.dataClasses.TrackCoordinatesData +import com.example.trackpro.managerClasses.utilities.haversineDistance +import kotlin.math.abs import kotlin.math.sqrt object TrackGeometry { @@ -48,6 +50,48 @@ object TrackGeometry { } } + /** + * Returns a copy of [track] with sector markers placed automatically at [sectorCount] even + * distance intervals along the path (e.g. sectorCount=3 marks the two points closest to + * 1/3 and 2/3 of the total distance). Any existing sector markers are cleared first, so + * calling this again with a different count simply replaces the previous slicing. Points + * are matched back to the DB by id, so this is safe to persist with an @Update. + */ + fun autoSliceSectors(track: List, sectorCount: Int): List { + if (sectorCount < 2 || track.size < sectorCount) { + return track.map { it.copy(isSectorPoint = false, sectorIndex = null) } + } + + val cumulative = DoubleArray(track.size) + for (i in 1 until track.size) { + cumulative[i] = cumulative[i - 1] + haversineDistance( + track[i - 1].latitude, track[i - 1].longitude, + track[i].latitude, track[i].longitude + ) + } + val totalDist = cumulative.last() + if (totalDist <= 0.0) { + return track.map { it.copy(isSectorPoint = false, sectorIndex = null) } + } + + // One boundary per internal division point (sectorCount-1 boundaries -> sectorCount sectors) + val boundaryIndices = (1 until sectorCount).map { k -> + val target = totalDist * k / sectorCount + cumulative.indices.minByOrNull { abs(cumulative[it] - target) } ?: 0 + }.distinct().sorted() + + return track.mapIndexed { i, point -> + val sectorIdx = boundaryIndices.indexOf(i) + if (sectorIdx >= 0) { + point.copy(isSectorPoint = true, sectorIndex = sectorIdx) + } else if (point.isSectorPoint) { + point.copy(isSectorPoint = false, sectorIndex = null) + } else { + point + } + } + } + /** * Builds a perpendicular gate line through [atPoint], oriented to the track's local * direction of travel around it (same construction as the finish/sector lines), or null 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 22202f7..598ed80 100644 --- a/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt @@ -41,6 +41,7 @@ import androidx.core.graphics.toColorInt import com.example.trackpro.TrackProApp import com.example.trackpro.extrasForUI.TrackProTheme import com.example.trackpro.managerClasses.JsonReader +import com.example.trackpro.managerClasses.utilities.UnitFormatter import kotlin.math.cos import kotlin.math.sin @@ -53,6 +54,7 @@ fun ESPConnectionTestScreen() { 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() // 2. Configuration for display val config = remember { JsonReader.loadConfig(context) } @@ -120,12 +122,12 @@ fun ESPConnectionTestScreen() { ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { StyledSpeedometer( - speed = speed, + speed = UnitFormatter.convertSpeed(speed, useMetric), textPrimary = TrackProTheme.colors.textPrimary ) Spacer(Modifier.height(8.dp)) Text( - text = "km/h", + text = UnitFormatter.speedUnitLabel(useMetric).lowercase(), color = TrackProTheme.colors.textMuted, fontSize = 12.sp, letterSpacing = 3.sp, 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 657dee2..6524e6b 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt @@ -373,12 +373,15 @@ suspend fun endTrackBuilder(context: Context, trackId: Long, isLapTrack: Boolean // Explicitly calculate total distance (blocking inside suspend) val helper = DragTimeCalculation(database = database) - val totalLength = helper.totalDistance(latlon) - Log.d("Total length (meters):", totalLength.toString()) + val totalLengthMeters = helper.totalDistance(latlon) + // TrackMainData.totalLength is stored in kilometers - matches the bundled seed tracks + // (see res/raw/tracks.json), which are authored in km. + val totalLengthKm = totalLengthMeters / 1000.0 + Log.d("Total length (km):", totalLengthKm.toString()) // Perform database update on IO dispatcher (ensure proper thread) withContext(Dispatchers.IO) { - val affectedRows = database.trackMainDao().updateTotalLength(trackId, totalLength) + val affectedRows = database.trackMainDao().updateTotalLength(trackId, totalLengthKm) Log.d("DB Update", "Updated totalLength on trackId=$trackId, affected rows: $affectedRows") } } 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 3f693b3..fae8434 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt @@ -1,6 +1,8 @@ package com.example.trackpro.screens import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,16 +13,19 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -37,6 +42,8 @@ import com.example.trackpro.dataClasses.TrackCoordinatesData import com.example.trackpro.dataClasses.TrackMainData import com.example.trackpro.extrasForUI.TrackProTheme import com.example.trackpro.managerClasses.ESPDatabase +import com.example.trackpro.managerClasses.timeAttackManagers.TrackGeometry +import com.example.trackpro.managerClasses.utilities.UnitFormatter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.maplibre.android.MapLibre @@ -61,6 +68,9 @@ fun TrackScreen(trackId: Long) { @Composable fun TrackView(database: ESPDatabase, trackId: Long) { + val app = LocalContext.current.applicationContext as TrackProApp + val useMetric by app.useMetricUnits.collectAsState() + val coroutineScope = rememberCoroutineScope() val trackParts = remember { mutableStateListOf() } val trackInfo = remember { mutableStateOf( @@ -136,7 +146,9 @@ fun TrackView(database: ESPDatabase, trackId: Long) { ) { TrackStatCol( label = "LENGTH", - value = "${trackInfo.value.totalLength ?: "?"} km", + // totalLength is stored in km; formatDistance takes meters, so convert + // first. This also makes the unit dynamic instead of a hardcoded "km". + value = trackInfo.value.totalLength?.let { UnitFormatter.formatDistance(it * 1000.0, useMetric) } ?: "?", textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted ) @@ -157,6 +169,26 @@ fun TrackView(database: ESPDatabase, trackId: Long) { HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) + // ── Sectors card ──────────────────────────────────── + if (trackParts.isNotEmpty()) { + SectorSlicerCard( + trackParts = trackParts, + onSlice = { sectorCount -> + val sliced = TrackGeometry.autoSliceSectors(trackParts.toList(), sectorCount) + coroutineScope.launch(Dispatchers.IO) { + database.trackCoordinatesDao().updateTrackCoordinates(sliced) + } + }, + onClear = { + val cleared = TrackGeometry.autoSliceSectors(trackParts.toList(), 1) + coroutineScope.launch(Dispatchers.IO) { + database.trackCoordinatesDao().updateTrackCoordinates(cleared) + } + } + ) + HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) + } + // ── Map ─────────────────────────────────────────── Box( modifier = Modifier @@ -199,6 +231,92 @@ private fun TrackStatCol(label: String, value: String, textPrimary: Color, textM } } +/** + * Lets the user auto-slice an already-saved track into N even sectors after the fact, + * instead of only being able to mark them by hand while live-recording. Picking a count + * replaces any previous slicing (the points are matched back to the DB by id). + */ +@Composable +private fun SectorSlicerCard( + trackParts: List, + onSlice: (Int) -> Unit, + onClear: () -> Unit +) { + val markedBoundaries = trackParts.count { it.isSectorPoint } + val sectorCount = if (markedBoundaries > 0) markedBoundaries + 1 else 0 + + Column( + modifier = Modifier + .fillMaxWidth() + .background(TrackProTheme.colors.bgCard) + .padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "SECTORS", + color = TrackProTheme.colors.textMuted, + fontSize = 10.sp, + letterSpacing = 2.sp, + fontWeight = FontWeight.Bold + ) + Text( + text = if (sectorCount > 0) "$sectorCount SECTORS MARKED" else "NONE MARKED", + color = if (sectorCount > 0) TrackProTheme.colors.accentBlue else TrackProTheme.colors.textMuted, + fontSize = 10.sp, + fontWeight = FontWeight.Black + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + (2..6).forEach { n -> + val active = sectorCount == n + Box( + modifier = Modifier + .weight(1f) + .background( + if (active) TrackProTheme.colors.accentBlue else TrackProTheme.colors.bgElevated, + RoundedCornerShape(6.dp) + ) + .border( + 1.dp, + if (active) Color.Transparent else TrackProTheme.colors.sectorLine, + RoundedCornerShape(6.dp) + ) + .clickable { onSlice(n) } + .padding(vertical = 10.dp), + contentAlignment = Alignment.Center + ) { + Text( + "$n", + color = if (active) Color.Black else TrackProTheme.colors.textPrimary, + fontWeight = FontWeight.Black, + fontSize = 14.sp + ) + } + } + } + + if (sectorCount > 0) { + Text( + text = "CLEAR SECTORS", + color = TrackProTheme.colors.deltaBad, + fontSize = 10.sp, + fontWeight = FontWeight.Black, + letterSpacing = 1.sp, + modifier = Modifier.clickable { onClear() } + ) + } + } +} + @Composable fun TrackStaticMapView( trackParts: List, @@ -206,7 +324,105 @@ fun TrackStaticMapView( modifier: Modifier = Modifier ) { var mapViewRef by remember { mutableStateOf(null) } - val trackDrawn = remember { mutableStateOf(false) } + var mapRef by remember { mutableStateOf(null) } + var styleRef by remember { mutableStateOf(null) } + var hasFitCamera by remember { mutableStateOf(false) } + + // Redraw whenever the track's points change (e.g. after auto-slicing sectors) instead + // of only ever drawing once on first load. trackParts is a SnapshotStateList mutated + // in place, so its reference never changes - key on derived values instead, or this + // would never restart. + val sectorMarkCount = trackParts.count { it.isSectorPoint } + LaunchedEffect(trackParts.size, sectorMarkCount) { + val style = styleRef ?: return@LaunchedEffect + val map = mapRef ?: return@LaunchedEffect + if (trackParts.isEmpty()) return@LaunchedEffect + + listOf("track-static-layer", "start-layer", "sector-layer").forEach { id -> + style.getLayer(id)?.let { style.removeLayer(it) } + } + listOf("track-static-src", "start-src", "sector-src").forEach { id -> + style.getSource(id)?.let { style.removeSource(it) } + } + + // A single (or empty) point can't form a line or a real bounding box - draw only + // the start marker and center on it directly, rather than feeding MapLibre a + // degenerate LineString/bounds. + if (trackParts.size >= 2) { + val coords = trackParts.joinToString(",") { + "[${it.longitude},${it.latitude}]" + } + + // Only close the loop if it's a Circuit + val finalCoordinates = if (trackType == "Circuit") { + val first = trackParts.first() + "$coords,[${first.longitude},${first.latitude}]" + } else { + coords // Keep it as an open line for Sprints + } + + val geojson = """{"type":"Feature","geometry":{"type":"LineString","coordinates":[$finalCoordinates]},"properties":{}}""" + style.addSource(GeoJsonSource("track-static-src", geojson)) + style.addLayer( + LineLayer("track-static-layer", "track-static-src").apply { + setProperties( + PropertyFactory.lineColor("#00C853"), + PropertyFactory.lineWidth(3f), + PropertyFactory.lineCap(Property.LINE_CAP_ROUND) + ) + } + ) + } + + // Start/finish marker - a single point at the actual start coordinate + val startPoint = trackParts.first() + val startGeojson = """{"type":"Feature","geometry":{"type":"Point","coordinates":[${startPoint.longitude},${startPoint.latitude}]},"properties":{}}""" + style.addSource(GeoJsonSource("start-src", startGeojson)) + style.addLayer( + CircleLayer("start-layer", "start-src").apply { + setProperties( + PropertyFactory.circleColor("#E8001C"), + PropertyFactory.circleRadius(8f), + PropertyFactory.circleStrokeColor("#FFFFFF"), + PropertyFactory.circleStrokeWidth(2f) + ) + } + ) + + // Sector markers, if any have been marked/auto-sliced + val sectorPoints = trackParts.filter { it.isSectorPoint } + if (sectorPoints.isNotEmpty()) { + val sectorFeatures = sectorPoints.joinToString(",") { + """{"type":"Feature","geometry":{"type":"Point","coordinates":[${it.longitude},${it.latitude}]},"properties":{}}""" + } + style.addSource(GeoJsonSource("sector-src", """{"type":"FeatureCollection","features":[$sectorFeatures]}""")) + style.addLayer( + CircleLayer("sector-layer", "sector-src").apply { + setProperties( + PropertyFactory.circleColor("#FBBF24"), + PropertyFactory.circleRadius(6f), + PropertyFactory.circleStrokeColor("#FFFFFF"), + PropertyFactory.circleStrokeWidth(1.5f) + ) + } + ) + } + + // Only fit the camera once - re-fitting on every sector change would be jarring. + if (!hasFitCamera) { + if (trackParts.size >= 2) { + val bounds = LatLngBounds.Builder() + .includes(trackParts.map { LatLng(it.latitude, it.longitude) }) + .build() + map.easeCamera(CameraUpdateFactory.newLatLngBounds(bounds, 64), 800) + } else { + map.moveCamera( + CameraUpdateFactory.newLatLngZoom(LatLng(startPoint.latitude, startPoint.longitude), 16.0) + ) + } + hasFitCamera = true + } + } AndroidView( factory = { ctx -> @@ -215,67 +431,13 @@ fun TrackStaticMapView( mapViewRef = mv mv.onCreate(null) mv.getMapAsync { map -> + mapRef = map map.setStyle("https://tiles.openfreemap.org/styles/dark") { style -> map.uiSettings.setAllGesturesEnabled(true) map.uiSettings.isCompassEnabled = false map.uiSettings.isLogoEnabled = false map.uiSettings.isAttributionEnabled = false - - // A single (or empty) point can't form a line or a real bounding - // box - draw only the start marker and center on it directly, - // rather than feeding MapLibre a degenerate LineString/bounds. - if (trackParts.size >= 2) { - val coords = trackParts.joinToString(",") { - "[${it.longitude},${it.latitude}]" - } - - // Only close the loop if it's a Circuit - val finalCoordinates = if (trackType == "Circuit") { - val first = trackParts.first() - "$coords,[${first.longitude},${first.latitude}]" - } else { - coords // Keep it as an open line for Sprints - } - - val geojson = """{"type":"Feature","geometry":{"type":"LineString","coordinates":[$finalCoordinates]},"properties":{}}""" - style.addSource(GeoJsonSource("track-static-src", geojson)) - style.addLayer( - LineLayer("track-static-layer", "track-static-src").apply { - setProperties( - PropertyFactory.lineColor("#00C853"), - PropertyFactory.lineWidth(3f), - PropertyFactory.lineCap(Property.LINE_CAP_ROUND) - ) - } - ) - } - - // Start/finish marker - a single point at the actual start coordinate - val startPoint = trackParts.first() - val startGeojson = """{"type":"Feature","geometry":{"type":"Point","coordinates":[${startPoint.longitude},${startPoint.latitude}]},"properties":{}}""" - style.addSource(GeoJsonSource("start-src", startGeojson)) - style.addLayer( - CircleLayer("start-layer", "start-src").apply { - setProperties( - PropertyFactory.circleColor("#E8001C"), - PropertyFactory.circleRadius(8f), - PropertyFactory.circleStrokeColor("#FFFFFF"), - PropertyFactory.circleStrokeWidth(2f) - ) - } - ) - - if (trackParts.size >= 2) { - val bounds = LatLngBounds.Builder() - .includes(trackParts.map { LatLng(it.latitude, it.longitude) }) - .build() - map.easeCamera(CameraUpdateFactory.newLatLngBounds(bounds, 64), 800) - } else { - map.moveCamera( - CameraUpdateFactory.newLatLngZoom(LatLng(startPoint.latitude, startPoint.longitude), 16.0) - ) - } - trackDrawn.value = true + styleRef = style } } } 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 4cc8a63..05b8d3e 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 @@ -41,9 +41,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import com.example.trackpro.TrackProApp import com.example.trackpro.dataClasses.TrackMainData import com.example.trackpro.extrasForUI.TrackProTheme import com.example.trackpro.managerClasses.ESPDatabase +import com.example.trackpro.managerClasses.utilities.UnitFormatter import com.example.trackpro.viewModels.TrackViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -53,6 +55,8 @@ import kotlinx.coroutines.withContext fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) { val tracks by viewModel.tracks.collectAsState() val context = LocalContext.current + val app = context.applicationContext as TrackProApp + val useMetric by app.useMetricUnits.collectAsState() val database = remember { ESPDatabase.getInstance(context) } val scope = rememberCoroutineScope() @@ -114,6 +118,7 @@ fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) { track = track, navController = navController, database = database, + useMetric = useMetric, bgCard = TrackProTheme.colors.bgCard, bgElevated = TrackProTheme.colors.bgElevated, accentAmber = TrackProTheme.colors.accentAmber, @@ -139,6 +144,7 @@ fun TrackCard( track: TrackMainData, navController: NavController, database: ESPDatabase, + useMetric: Boolean, bgCard: Color, bgElevated: Color, accentAmber: Color, @@ -278,7 +284,8 @@ fun TrackCard( Text("LENGTH", color = textMuted, fontSize = 9.sp, letterSpacing = 1.sp, fontWeight = FontWeight.Bold) Text( - text = "${track.totalLength ?: "?"} km", + // totalLength is stored in km; formatDistance takes meters. + text = track.totalLength?.let { UnitFormatter.formatDistance(it * 1000.0, useMetric) } ?: "?", color = accentAmber, fontSize = 16.sp, fontWeight = FontWeight.Black 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 609cca4..1b82f6d 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 @@ -17,6 +17,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -29,9 +30,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.trackpro.TrackProApp import com.example.trackpro.dataClasses.VehicleInformationData import com.example.trackpro.extrasForUI.TrackProTheme import com.example.trackpro.managerClasses.ESPDatabase +import com.example.trackpro.managerClasses.utilities.UnitFormatter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -39,6 +42,8 @@ import kotlinx.coroutines.withContext @Composable fun CarViewScreen(vehicleId: Long) { val context = LocalContext.current + val app = context.applicationContext as TrackProApp + val useMetric by app.useMetricUnits.collectAsState() val database = remember { ESPDatabase.getInstance(context) } var vehicleInfo by remember { mutableStateOf(null) } @@ -163,8 +168,8 @@ fun CarViewScreen(vehicleId: Long) { ) { VehicleStatCol( label = "TOP SPEED", - value = vehicle.topSpeed?.toString() ?: "—", - unit = "km/h", + value = vehicle.topSpeed?.let { UnitFormatter.formatSpeed(it, useMetric) } ?: "—", + unit = UnitFormatter.speedUnitLabel(useMetric).lowercase(), accentColor = TrackProTheme.colors.accentAmber, textMuted = TrackProTheme.colors.textMuted ) 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 c6d65a5..ef25a63 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 @@ -26,6 +26,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -41,6 +42,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import androidx.room.Room +import com.example.trackpro.TrackProApp import com.example.trackpro.dataClasses.LapInfoData import com.example.trackpro.dataClasses.LapTimeData import com.example.trackpro.dataClasses.SessionData @@ -48,6 +50,7 @@ import com.example.trackpro.dataClasses.VehicleInformationData import com.example.trackpro.extrasForUI.TrackProTheme import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.managerClasses.utilities.DateFormatterUtil +import com.example.trackpro.managerClasses.utilities.UnitFormatter import com.example.trackpro.managerClasses.utilities.toLapTimeMillis import com.example.trackpro.managerClasses.utilities.toLapTimeString import kotlinx.coroutines.Dispatchers @@ -80,6 +83,9 @@ fun TimeAttackListItemScreen( database: ESPDatabase, sessionId: Long ) { + val app = LocalContext.current.applicationContext as TrackProApp + val useMetric by app.useMetricUnits.collectAsState() + var sessionData by remember { mutableStateOf(null) } var vehicleData by remember { mutableStateOf(null) } var lapTimes by remember { mutableStateOf>(emptyList()) } @@ -268,7 +274,7 @@ fun TimeAttackListItemScreen( ) StatRowItem( label = "TOP SPEED (SESSION)", - value = String.format("%.1f km/h", topSpeedOverall), + value = "${UnitFormatter.formatSpeedPrecise(topSpeedOverall.toDouble(), useMetric)} ${UnitFormatter.speedUnitLabel(useMetric)}", textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted ) @@ -336,6 +342,7 @@ fun TimeAttackListItemScreen( isWorst = isWorst, deltaMs = deltaMs, topSpeed = topSpeed, + useMetric = useMetric, bgCard = TrackProTheme.colors.bgCard, bgElevated = TrackProTheme.colors.bgElevated, accentGreen = TrackProTheme.colors.accentBlue, @@ -362,6 +369,7 @@ private fun LapRow( isWorst: Boolean, deltaMs: Long, topSpeed: Float, + useMetric: Boolean, bgCard: Color, bgElevated: Color, accentGreen: Color, @@ -429,13 +437,13 @@ private fun LapRow( Text("TOP SPEED", color = textMuted, fontSize = 8.sp, letterSpacing = 1.sp, fontWeight = FontWeight.Bold) Text( - text = if (topSpeed > 0) String.format("%.0f", topSpeed) else "—", + text = if (topSpeed > 0) UnitFormatter.formatSpeed(topSpeed, useMetric) else "—", color = textPrimary, fontSize = 16.sp, fontWeight = FontWeight.Bold ) if (topSpeed > 0) { - Text("km/h", color = textMuted, fontSize = 8.sp) + Text(UnitFormatter.speedUnitLabel(useMetric).lowercase(), color = textMuted, fontSize = 8.sp) } } 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 f41d002..261cf45 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 @@ -51,6 +51,7 @@ import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.managerClasses.SessionManager import com.example.trackpro.managerClasses.calculationClasses.DragMetrics import com.example.trackpro.managerClasses.calculationClasses.DragTimeCalculation +import com.example.trackpro.managerClasses.utilities.UnitFormatter import com.example.trackpro.viewModels.VehicleFULLViewModel import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.components.XAxis @@ -80,6 +81,7 @@ fun DragRaceScreen( ) { val app = LocalContext.current.applicationContext as TrackProApp val scope = rememberCoroutineScope() + val useMetric by app.useMetricUnits.collectAsState() // --- GPS & CONNECTION STATE --- val isConnected by app.gpsManager.connectionStatus.collectAsState(initial = false) @@ -322,7 +324,7 @@ fun DragRaceScreen( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - "${gpsData?.speed?.toInt() ?: 0}", + gpsData?.speed?.let { UnitFormatter.formatSpeed(it, useMetric) } ?: "0", fontSize = 72.sp, fontWeight = FontWeight.Black, color = if (isSessionActive) TrackProTheme.colors.accentCyan @@ -330,7 +332,7 @@ fun DragRaceScreen( letterSpacing = (-2).sp ) Text( - "KM/H", + UnitFormatter.speedUnitLabel(useMetric), fontSize = 16.sp, fontWeight = FontWeight.Bold, color = TrackProTheme.colors.textMuted, @@ -341,7 +343,7 @@ fun DragRaceScreen( if (isSessionActive) { Spacer(Modifier.height(8.dp)) Text( - "MAX: ${currentMetrics.maxSpeed.toInt()} KM/H · DIST: ${String.format("%.1f", currentMetrics.totalDistance)}M", + "MAX: ${UnitFormatter.formatSpeed(currentMetrics.maxSpeed, useMetric)} ${UnitFormatter.speedUnitLabel(useMetric)} · DIST: ${UnitFormatter.formatDistance(currentMetrics.totalDistance.toDouble(), useMetric)}", color = TrackProTheme.colors.textMuted, fontSize = 13.sp, fontWeight = FontWeight.Bold @@ -398,8 +400,8 @@ fun DragRaceScreen( ), DragMetricDisplay( "1/4 TRAP", - currentMetrics.quarterMileSpeed?.toInt()?.toString() ?: "--", - "KM/H", + currentMetrics.quarterMileSpeed?.let { UnitFormatter.formatSpeed(it, useMetric) } ?: "--", + UnitFormatter.speedUnitLabel(useMetric), currentMetrics.quarterMileSpeed != null ), ) 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 ba744ff..e1454c1 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 @@ -88,6 +88,7 @@ fun TimeAttackScreenView( val bestTime by vm.bestTime.collectAsState() val lastTime by vm.lastTime.collectAsState() val delta by vm.delta.collectAsState() + val liveDelta by vm.liveDelta.collectAsState() val eventCount by vm.eventCount.collectAsState() val stintStart by vm.stintStart.collectAsState() val fullTrack by vm.fullTrack.collectAsState() @@ -97,6 +98,11 @@ fun TimeAttackScreenView( val timingMode by vm.timingMode.collectAsState() val lapSplits by vm.currentLapSplits.collectAsState() + // Prefer the continuously-updating delta (tracked by distance into the lap); fall back + // to the static per-lap delta before a best-lap reference exists (e.g. lap 1). + val effectiveDelta = liveDelta ?: delta + val isLiveDelta = liveDelta != null + val linesToShow by remember(timingMode, startLine, finishLine) { derivedStateOf { if (timingMode is TimingMode.Sprint) startLine + finishLine else finishLine @@ -157,7 +163,8 @@ fun TimeAttackScreenView( currentTime = currentTime, bestTime = bestTime, lastTime = lastTime, - delta = delta, + delta = effectiveDelta, + isLiveDelta = isLiveDelta, eventCount = eventCount, stintStart = stintStart, gpsPoints = gpsPoints, @@ -171,7 +178,8 @@ fun TimeAttackScreenView( currentTime = currentTime, bestTime = bestTime, lastTime = lastTime, - delta = delta, + delta = effectiveDelta, + isLiveDelta = isLiveDelta, eventCount = eventCount, stintStart = stintStart, gpsPoints = gpsPoints, @@ -192,6 +200,7 @@ fun TimeAttackPortraitLayout( bestTime: String, lastTime: String, delta: Double, + isLiveDelta: Boolean = false, eventCount: Int, stintStart: Long, gpsPoints: List, @@ -264,17 +273,29 @@ fun TimeAttackPortraitLayout( letterSpacing = (-1).sp, lineHeight = 68.sp ) - Box( - modifier = Modifier - .background(deltaColor.copy(alpha = 0.15f), RoundedCornerShape(4.dp)) - .padding(horizontal = 10.dp, vertical = 4.dp) - ) { - Text( - text = "Δ ${String.format("%+.3f", delta)}s", - color = deltaColor, - fontSize = 18.sp, - fontWeight = FontWeight.Bold - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .background(deltaColor.copy(alpha = 0.15f), RoundedCornerShape(4.dp)) + .padding(horizontal = 10.dp, vertical = 4.dp) + ) { + Text( + text = "Δ ${String.format("%+.3f", delta)}s", + color = deltaColor, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } + if (isLiveDelta) { + Spacer(Modifier.width(6.dp)) + Text( + text = "● LIVE", + color = deltaColor.copy(alpha = 0.7f), + fontSize = 9.sp, + fontWeight = FontWeight.Black, + letterSpacing = 1.sp + ) + } } } Column( @@ -364,6 +385,7 @@ fun TimeAttackLandscapeLayout( bestTime: String, lastTime: String, delta: Double, + isLiveDelta: Boolean = false, eventCount: Int, stintStart: Long, gpsPoints: List, @@ -429,17 +451,29 @@ fun TimeAttackLandscapeLayout( fontWeight = FontWeight.Black, letterSpacing = (-1).sp ) - Box( - modifier = Modifier - .background(deltaColor.copy(alpha = 0.15f), RoundedCornerShape(4.dp)) - .padding(horizontal = 8.dp, vertical = 3.dp) - ) { - Text( - text = "Δ ${String.format("%+.3f", delta)}s", - color = deltaColor, - fontSize = 14.sp, - fontWeight = FontWeight.Bold - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .background(deltaColor.copy(alpha = 0.15f), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 3.dp) + ) { + Text( + text = "Δ ${String.format("%+.3f", delta)}s", + color = deltaColor, + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + if (isLiveDelta) { + Spacer(Modifier.width(4.dp)) + Text( + text = "● LIVE", + color = deltaColor.copy(alpha = 0.7f), + fontSize = 8.sp, + fontWeight = FontWeight.Black, + letterSpacing = 1.sp + ) + } } Spacer(Modifier.height(16.dp)) 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 43544f3..0c19880 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 @@ -27,6 +27,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,12 +40,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.trackpro.TrackProApp import com.example.trackpro.dataClasses.VehicleInformationData import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.extrasForUI.CustomTextField import com.example.trackpro.extrasForUI.DropdownMenuField import com.example.trackpro.extrasForUI.TrackProTheme import com.example.trackpro.managerClasses.JsonReader.loadJsonOptions +import com.example.trackpro.managerClasses.utilities.UnitFormatter import kotlinx.coroutines.launch @Composable @@ -52,6 +55,8 @@ fun CarCreationScreen( database: ESPDatabase ) { val context = LocalContext.current + val app = context.applicationContext as TrackProApp + val useMetric by app.useMetricUnits.collectAsState() val jsonOptions = remember { loadJsonOptions(context) } val coroutineScope = rememberCoroutineScope() @@ -121,8 +126,11 @@ fun CarCreationScreen( 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 (km/h)", topSpeed, true, Icons.Default.Speed) { topSpeed = it } - CustomTextField("0-100 km/h (s)", acceleration, true, Icons.Default.Timer) { acceleration = it } + CustomTextField("Top Speed (${UnitFormatter.speedUnitLabel(useMetric)})", topSpeed, true, Icons.Default.Speed) { topSpeed = it } + CustomTextField( + if (useMetric) "0-100 KM/H (s)" else "0-60 MPH (s)", + acceleration, true, Icons.Default.Timer + ) { acceleration = it } CustomTextField("Fuel Capacity (L)", fuelCapacity, true, Icons.Default.LocalGasStation) { fuelCapacity = it } SectionTitle("Configuration") @@ -150,7 +158,9 @@ fun CarCreationScreen( horsepower = horsepower.toIntOrNull() ?: 0, torque = torque.toIntOrNull(), weight = weight.toDoubleOrNull() ?: 0.0, - topSpeed = topSpeed.toDoubleOrNull(), + // Stored canonically in km/h regardless of the unit the + // user entered it in, matching every other speed value. + topSpeed = topSpeed.toDoubleOrNull()?.let { UnitFormatter.convertSpeedToKmh(it, useMetric) }, acceleration = acceleration.toDoubleOrNull(), drivetrain = selectedDrivetrain, fuelType = selectedFuelType, diff --git a/app/src/main/java/com/example/trackpro/viewModels/TimeAttackViewModel.kt b/app/src/main/java/com/example/trackpro/viewModels/TimeAttackViewModel.kt index 2daf0f0..ba9d612 100644 --- a/app/src/main/java/com/example/trackpro/viewModels/TimeAttackViewModel.kt +++ b/app/src/main/java/com/example/trackpro/viewModels/TimeAttackViewModel.kt @@ -75,6 +75,13 @@ class TimeAttackViewModel( get() = (timingManager as? CircuitTimingManager)?.currentLapSplits ?: MutableStateFlow>(emptyList()).asStateFlow() + // Continuously-updating delta vs. the session's best lap, tracked by distance into the + // lap rather than only once at the finish line. Null until a best lap reference exists + // (i.e. before the first lap of the session has completed) or in Sprint mode. + val liveDelta: StateFlow + get() = (timingManager as? CircuitTimingManager)?.liveDelta + ?: MutableStateFlow(null).asStateFlow() + // Expose timing state val currentTime: StateFlow get() = timingManager?.currentTime ?: MutableStateFlow("00:00.00").asStateFlow() From f1ec0043ebc70e85567a6fda3c420633189283ab Mon Sep 17 00:00:00 2001 From: "meszaros.martin986@gmail.com" Date: Fri, 31 Jul 2026 09:23:38 +0200 Subject: [PATCH 3/5] Refined track recording and processing in TrackBuilderScreen This commit fixes a potential race condition during track saving and improves performance by offloading heavy track processing tasks to background threads. **Key Changes:** * **`TrackBuilderScreen.kt`:** * **Race Condition Fix:** The recording state (`isLiveRecording`) is now set to `false` and a snapshot of the `gpsPointsList` is created *before* launching the database insertion coroutine. This prevents Room from encountering concurrent modification exceptions if new GPS points arrive while the database transaction is being prepared. * **Threading Optimization:** In `endTrackBuilder`, the CPU-intensive track post-processing, coordinate mapping, and distance calculations have been wrapped in `withContext(Dispatchers.Default)` to prevent blocking the UI thread during long recordings. * Updated the database update call within `endTrackBuilder` to ensure it remains on the `IO` dispatcher. * Minor code cleanup and improved commenting regarding thread safety and performance. --- .../trackpro/screens/TrackBuilderScreen.kt | 68 +++++++++++-------- 1 file changed, 39 insertions(+), 29 deletions(-) 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 6524e6b..8578222 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt @@ -136,12 +136,18 @@ fun TrackBuilderScreen( } } } else { + // Stop recording immediately (synchronously, before the save + // coroutine even launches) so the GPS-update effect above stops + // appending to gpsPointsList while we're reading/persisting it + // below - otherwise Room can be iterating the list to build the + // insert at the same moment new points are still being added to + // it, which aborts the whole transaction and saves nothing. + isLiveRecording = false + val pointsToSave = gpsPointsList.toList() coroutineScope.launch { - // Save the points collected in the list to the DB - database.trackCoordinatesDao().insertTrackPart(gpsPointsList) + database.trackCoordinatesDao().insertTrackPart(pointsToSave) val isLapTrack = (trackMode == "Circuit") - endTrackBuilder(context, trackID,isLapTrack) - isLiveRecording = false + endTrackBuilder(context, trackID, isLapTrack) onBack() } } @@ -356,33 +362,37 @@ suspend fun endTrackBuilder(context: Context, trackId: Long, isLapTrack: Boolean val database = ESPDatabase.getInstance(context) val postProcess = PostProcessing(database) + // The dedup/lap-detection/distance loops below are CPU-bound and can take a + // noticeable amount of time for a longer recording (thousands of points) - run them + // off the caller's dispatcher so they don't block the UI thread. + withContext(Dispatchers.Default) { + // Explicitly wait and ensure the processed track data is retrieved + val track: List = postProcess.processTrackPoints(trackId, isLapTrack) - // Explicitly wait and ensure the processed track data is retrieved - val track: List = postProcess.processTrackPoints(trackId,isLapTrack) - - if (track.isEmpty()) { - Log.w("endTrackBuilder", "No track points found for trackId=$trackId! Aborting.") - return - } + if (track.isEmpty()) { + Log.w("endTrackBuilder", "No track points found for trackId=$trackId! Aborting.") + return@withContext + } - // Map to lat/lon offsets (synchronously after suspend) - val latlon: List = track.map { point -> - LatLonOffset(lat = point.latitude, lon = point.longitude) - } - Log.d("latlon:", latlon.toString()) - - // Explicitly calculate total distance (blocking inside suspend) - val helper = DragTimeCalculation(database = database) - val totalLengthMeters = helper.totalDistance(latlon) - // TrackMainData.totalLength is stored in kilometers - matches the bundled seed tracks - // (see res/raw/tracks.json), which are authored in km. - val totalLengthKm = totalLengthMeters / 1000.0 - Log.d("Total length (km):", totalLengthKm.toString()) - - // Perform database update on IO dispatcher (ensure proper thread) - withContext(Dispatchers.IO) { - val affectedRows = database.trackMainDao().updateTotalLength(trackId, totalLengthKm) - Log.d("DB Update", "Updated totalLength on trackId=$trackId, affected rows: $affectedRows") + // Map to lat/lon offsets (synchronously after suspend) + val latlon: List = track.map { point -> + LatLonOffset(lat = point.latitude, lon = point.longitude) + } + Log.d("latlon:", latlon.toString()) + + // Explicitly calculate total distance (blocking inside suspend) + val helper = DragTimeCalculation(database = database) + val totalLengthMeters = helper.totalDistance(latlon) + // TrackMainData.totalLength is stored in kilometers - matches the bundled seed tracks + // (see res/raw/tracks.json), which are authored in km. + val totalLengthKm = totalLengthMeters / 1000.0 + Log.d("Total length (km):", totalLengthKm.toString()) + + // Perform database update on IO dispatcher (ensure proper thread) + withContext(Dispatchers.IO) { + val affectedRows = database.trackMainDao().updateTotalLength(trackId, totalLengthKm) + Log.d("DB Update", "Updated totalLength on trackId=$trackId, affected rows: $affectedRows") + } } } From 3b525f9742f3539050d05123e4758fcc1fa98fda Mon Sep 17 00:00:00 2001 From: "meszaros.martin986@gmail.com" Date: Fri, 31 Jul 2026 11:22:49 +0200 Subject: [PATCH 4/5] Refactored track seeding logic and added new circuit data This commit introduces a more robust mechanism for syncing premade tracks and expands the built-in track database. It also improves the reliability of track rendering on the map. **Key Changes:** * **Track Data Updates (`tracks.json`):** * Added comprehensive coordinate data for **Hungaroring** (Hungary), **Mugello** (Italy), and **Circuit Paul Ricard** (France). * **Database & Seeding Logic:** * **`TrackSeeder.kt` & `MainActivity.kt`:** Moved track seeding out of the `RoomDatabase.Callback` into a dedicated `TrackSeeder`. The app now syncs bundled tracks on every launch (using name-based deduplication) to ensure existing users receive new track updates. * **`TrackMainDataDAO.kt`:** Added `getAllTrackNames()` to facilitate idempotent seeding. * **`ESPDatabase.kt`:** Removed the legacy `onCreate` seeding logic in favor of the new `TrackSeeder` approach and enabled `fallbackToDestructiveMigration`. * **UI & Rendering Improvements:** * **`TrackScreen.kt`:** Updated `LaunchedEffect` keys in the map rendering logic to include `mapRef` and `styleRef`. This ensures tracks are correctly drawn even if the data loads before the map interface is fully initialized. * **`SettingsScreen.kt`:** Added an attribution notice for OpenStreetMap contributors. * **General:** * Cleaned up unused imports and logging in `ESPDatabase.kt`. --- .../java/com/example/trackpro/MainActivity.kt | 7 + .../example/trackpro/dao/TrackMainDataDAO.kt | 4 + .../trackpro/managerClasses/ESPDatabase.kt | 62 +- .../trackpro/screens/SettingsScreen.kt | 1 + .../example/trackpro/screens/TrackScreen.kt | 10 +- app/src/main/res/raw/tracks.json | 2050 ++++++++++++++++- 6 files changed, 2075 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/example/trackpro/MainActivity.kt b/app/src/main/java/com/example/trackpro/MainActivity.kt index caba154..9bcb9ed 100644 --- a/app/src/main/java/com/example/trackpro/MainActivity.kt +++ b/app/src/main/java/com/example/trackpro/MainActivity.kt @@ -60,6 +60,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController 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.JsonReader import com.example.trackpro.managerClasses.SessionManager @@ -143,6 +144,12 @@ class TrackProApp : Application() { MapLibre.getInstance(this) // Start the active provider immediately at app launch gpsManager.startActiveProvider() + + // Sync bundled premade tracks on every launch (not just first install) so existing + // users pick up newly-added ones too; name-deduped, so this is always safe to re-run. + applicationScope.launch(Dispatchers.IO) { + TrackSeeder.syncPremadeTracks(this@TrackProApp, database) + } } override fun onTerminate() { diff --git a/app/src/main/java/com/example/trackpro/dao/TrackMainDataDAO.kt b/app/src/main/java/com/example/trackpro/dao/TrackMainDataDAO.kt index f4a008b..e34a658 100644 --- a/app/src/main/java/com/example/trackpro/dao/TrackMainDataDAO.kt +++ b/app/src/main/java/com/example/trackpro/dao/TrackMainDataDAO.kt @@ -15,6 +15,10 @@ interface TrackMainDataDAO { @Query("SELECT * FROM track_main_data ORDER BY trackName ASC") fun getAllTrack(): Flow> + // One-shot (non-Flow) name lookup used to dedup premade-track seeding on startup. + @Query("SELECT trackName FROM track_main_data") + suspend fun getAllTrackNames(): List + @Query("Select * from track_main_data where trackId =:trackId") fun getTrack(trackId: Long): Flow 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 ebfb96b..900e7a2 100644 --- a/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt +++ b/app/src/main/java/com/example/trackpro/managerClasses/ESPDatabase.kt @@ -1,11 +1,9 @@ package com.example.trackpro.managerClasses import android.content.Context -import android.util.Log import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import androidx.sqlite.db.SupportSQLiteDatabase import com.example.trackpro.dao.DerivedDataDao import com.example.trackpro.dao.LapInfoDataDAO import com.example.trackpro.dao.LapTimeDataDAO @@ -24,15 +22,8 @@ import com.example.trackpro.dataClasses.SectorTimeData import com.example.trackpro.dataClasses.SessionData import com.example.trackpro.dataClasses.SmoothedGPSData import com.example.trackpro.dataClasses.TrackCoordinatesData -import com.example.trackpro.dataClasses.TrackJson import com.example.trackpro.dataClasses.TrackMainData import com.example.trackpro.dataClasses.VehicleInformationData -import com.example.trackpro.R -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Database(entities = [ @@ -70,54 +61,6 @@ abstract class ESPDatabase : RoomDatabase() { ESPDatabase::class.java, "esp_database" ) - .addCallback(object : Callback() { - override fun onCreate(db: SupportSQLiteDatabase) { - super.onCreate(db) - - CoroutineScope(Dispatchers.IO).launch { - try { - val inputStream = context.resources.openRawResource(R.raw.tracks) // your JSON file - val jsonString = inputStream.bufferedReader().use { it.readText() } - - val tracks: List = Gson().fromJson( - jsonString, - object : TypeToken>() {}.type - ) - - val db = getInstance(context) - - for (track in tracks) { - val trackId = db.trackMainDao().insertTrackMainDataDAO( - TrackMainData( - trackName = track.trackName, - totalLength = track.totalLength, - country = track.country, - type = track.type - ) - ) - - val coords = track.coordinates.mapIndexed { index, coord -> - TrackCoordinatesData( - trackId = trackId, - latitude = coord.lat, - longitude = coord.lon, - altitude = null, - isStartPoint = index == 0 - ) - } - - db.trackCoordinatesDao().insertTrack(coords) - - Log.d("DB_INIT", "Inserted ${coords.size} points for ${track.trackName}") - } - - } catch (e: Exception) { - Log.e("DB_INIT", "Error inserting tracks", e) - } - } - - } - }) // No migrations exist yet; without this, any future (or this) schema // change throws IllegalStateException on every existing install instead // of recovering. Replace with real Migration objects once the schema @@ -125,6 +68,11 @@ abstract class ESPDatabase : RoomDatabase() { .fallbackToDestructiveMigration() .build() + // Premade tracks (res/raw/tracks.json) are synced separately on every app + // start via TrackSeeder, called from TrackProApp.onCreate() - not here, since + // this factory can be called from a background thread and seeding is its own + // idempotent, name-deduped operation rather than a one-time DB-creation hook. + INSTANCE = instance instance } 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 0ab74dd..b8ca13a 100644 --- a/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt @@ -181,6 +181,7 @@ fun SettingsScreen(onBack: () -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { SettingsInfoRow(label = "App Version", value = "1.0.4-PRO") SettingsInfoRow(label = "Database Status", value = "Connected") + SettingsInfoRow(label = "Map & Track Data", value = "© OpenStreetMap contributors") } } } 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 fae8434..6cd1a1b 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt @@ -332,8 +332,16 @@ fun TrackStaticMapView( // of only ever drawing once on first load. trackParts is a SnapshotStateList mutated // in place, so its reference never changes - key on derived values instead, or this // would never restart. + // + // mapRef/styleRef are also keys, not just guards: getMapAsync/setStyle are async, and + // trackParts is normally already fully loaded *before* the map finishes initializing + // (this composable isn't even entered until trackParts is non-empty - see TrackView). + // Without these as keys, the effect's first run would hit them still null, bail out, + // and - since trackParts.size/sectorMarkCount never change again afterwards - never get + // a second chance to run once the map actually becomes ready, leaving the track + // permanently undrawn (default world view) despite the data being there. val sectorMarkCount = trackParts.count { it.isSectorPoint } - LaunchedEffect(trackParts.size, sectorMarkCount) { + LaunchedEffect(trackParts.size, sectorMarkCount, mapRef, styleRef) { val style = styleRef ?: return@LaunchedEffect val map = mapRef ?: return@LaunchedEffect if (trackParts.isEmpty()) return@LaunchedEffect diff --git a/app/src/main/res/raw/tracks.json b/app/src/main/res/raw/tracks.json index 814c249..88c9149 100644 --- a/app/src/main/res/raw/tracks.json +++ b/app/src/main/res/raw/tracks.json @@ -1323,5 +1323,2053 @@ "coordinates": [ ] + }, + { + "trackName": "Hungaroring", + "totalLength": 4.362, + "country": "Hungary", + "type": "Circuit", + "coordinates": [ + { + "lat": 47.577711, + "lon": 19.250593 + }, + { + "lat": 47.580336, + "lon": 19.245660 + }, + { + "lat": 47.580951, + "lon": 19.244509 + }, + { + "lat": 47.582052, + "lon": 19.242442 + }, + { + "lat": 47.582144, + "lon": 19.242323 + }, + { + "lat": 47.582250, + "lon": 19.242274 + }, + { + "lat": 47.582359, + "lon": 19.242304 + }, + { + "lat": 47.582450, + "lon": 19.242410 + }, + { + "lat": 47.582490, + "lon": 19.242586 + }, + { + "lat": 47.582500, + "lon": 19.242776 + }, + { + "lat": 47.582499, + "lon": 19.242942 + }, + { + "lat": 47.582490, + "lon": 19.243286 + }, + { + "lat": 47.582483, + "lon": 19.243452 + }, + { + "lat": 47.582463, + "lon": 19.243696 + }, + { + "lat": 47.582440, + "lon": 19.243873 + }, + { + "lat": 47.582412, + "lon": 19.244057 + }, + { + "lat": 47.582359, + "lon": 19.244324 + }, + { + "lat": 47.582305, + "lon": 19.244529 + }, + { + "lat": 47.582215, + "lon": 19.244811 + }, + { + "lat": 47.582093, + "lon": 19.245090 + }, + { + "lat": 47.582016, + "lon": 19.245243 + }, + { + "lat": 47.581813, + "lon": 19.245621 + }, + { + "lat": 47.581369, + "lon": 19.246453 + }, + { + "lat": 47.580913, + "lon": 19.247307 + }, + { + "lat": 47.580836, + "lon": 19.247470 + }, + { + "lat": 47.580796, + "lon": 19.247630 + }, + { + "lat": 47.580788, + "lon": 19.247800 + }, + { + "lat": 47.580817, + "lon": 19.247983 + }, + { + "lat": 47.580865, + "lon": 19.248134 + }, + { + "lat": 47.580964, + "lon": 19.248290 + }, + { + "lat": 47.581065, + "lon": 19.248371 + }, + { + "lat": 47.581173, + "lon": 19.248407 + }, + { + "lat": 47.581331, + "lon": 19.248411 + }, + { + "lat": 47.581451, + "lon": 19.248347 + }, + { + "lat": 47.582177, + "lon": 19.247741 + }, + { + "lat": 47.582285, + "lon": 19.247671 + }, + { + "lat": 47.582404, + "lon": 19.247622 + }, + { + "lat": 47.582548, + "lon": 19.247630 + }, + { + "lat": 47.582660, + "lon": 19.247687 + }, + { + "lat": 47.583566, + "lon": 19.248347 + }, + { + "lat": 47.585106, + "lon": 19.249468 + }, + { + "lat": 47.585226, + "lon": 19.249559 + }, + { + "lat": 47.585341, + "lon": 19.249651 + }, + { + "lat": 47.585446, + "lon": 19.249742 + }, + { + "lat": 47.585553, + "lon": 19.249841 + }, + { + "lat": 47.585669, + "lon": 19.249959 + }, + { + "lat": 47.585788, + "lon": 19.250094 + }, + { + "lat": 47.585924, + "lon": 19.250254 + }, + { + "lat": 47.586083, + "lon": 19.250482 + }, + { + "lat": 47.586194, + "lon": 19.250573 + }, + { + "lat": 47.586318, + "lon": 19.250593 + }, + { + "lat": 47.586431, + "lon": 19.250562 + }, + { + "lat": 47.587692, + "lon": 19.250051 + }, + { + "lat": 47.587834, + "lon": 19.250016 + }, + { + "lat": 47.587957, + "lon": 19.250015 + }, + { + "lat": 47.588076, + "lon": 19.250064 + }, + { + "lat": 47.588192, + "lon": 19.250148 + }, + { + "lat": 47.588279, + "lon": 19.250254 + }, + { + "lat": 47.588347, + "lon": 19.250384 + }, + { + "lat": 47.588403, + "lon": 19.250560 + }, + { + "lat": 47.588427, + "lon": 19.250720 + }, + { + "lat": 47.588421, + "lon": 19.250928 + }, + { + "lat": 47.588386, + "lon": 19.251105 + }, + { + "lat": 47.588205, + "lon": 19.251565 + }, + { + "lat": 47.588120, + "lon": 19.251746 + }, + { + "lat": 47.588035, + "lon": 19.251895 + }, + { + "lat": 47.587945, + "lon": 19.252045 + }, + { + "lat": 47.587823, + "lon": 19.252234 + }, + { + "lat": 47.586918, + "lon": 19.253481 + }, + { + "lat": 47.586801, + "lon": 19.253549 + }, + { + "lat": 47.586694, + "lon": 19.253516 + }, + { + "lat": 47.586598, + "lon": 19.253389 + }, + { + "lat": 47.586488, + "lon": 19.253303 + }, + { + "lat": 47.586346, + "lon": 19.253322 + }, + { + "lat": 47.585196, + "lon": 19.253782 + }, + { + "lat": 47.585078, + "lon": 19.253863 + }, + { + "lat": 47.584991, + "lon": 19.253976 + }, + { + "lat": 47.584930, + "lon": 19.254149 + }, + { + "lat": 47.584850, + "lon": 19.255126 + }, + { + "lat": 47.584821, + "lon": 19.255308 + }, + { + "lat": 47.584770, + "lon": 19.255453 + }, + { + "lat": 47.584682, + "lon": 19.255600 + }, + { + "lat": 47.584583, + "lon": 19.255692 + }, + { + "lat": 47.584463, + "lon": 19.255736 + }, + { + "lat": 47.584336, + "lon": 19.255729 + }, + { + "lat": 47.583184, + "lon": 19.255497 + }, + { + "lat": 47.583065, + "lon": 19.255483 + }, + { + "lat": 47.582955, + "lon": 19.255493 + }, + { + "lat": 47.582842, + "lon": 19.255521 + }, + { + "lat": 47.582736, + "lon": 19.255570 + }, + { + "lat": 47.582619, + "lon": 19.255651 + }, + { + "lat": 47.581802, + "lon": 19.256443 + }, + { + "lat": 47.581693, + "lon": 19.256523 + }, + { + "lat": 47.581576, + "lon": 19.256563 + }, + { + "lat": 47.581449, + "lon": 19.256566 + }, + { + "lat": 47.581323, + "lon": 19.256533 + }, + { + "lat": 47.581205, + "lon": 19.256465 + }, + { + "lat": 47.581078, + "lon": 19.256345 + }, + { + "lat": 47.580881, + "lon": 19.256098 + }, + { + "lat": 47.579703, + "lon": 19.254534 + }, + { + "lat": 47.578739, + "lon": 19.253253 + }, + { + "lat": 47.578665, + "lon": 19.253117 + }, + { + "lat": 47.578644, + "lon": 19.252958 + }, + { + "lat": 47.578679, + "lon": 19.252803 + }, + { + "lat": 47.579394, + "lon": 19.251819 + }, + { + "lat": 47.579490, + "lon": 19.251665 + }, + { + "lat": 47.579832, + "lon": 19.251030 + }, + { + "lat": 47.579890, + "lon": 19.250865 + }, + { + "lat": 47.579905, + "lon": 19.250688 + }, + { + "lat": 47.579888, + "lon": 19.250516 + }, + { + "lat": 47.579835, + "lon": 19.250367 + }, + { + "lat": 47.579754, + "lon": 19.250247 + }, + { + "lat": 47.579626, + "lon": 19.250173 + }, + { + "lat": 47.579501, + "lon": 19.250168 + }, + { + "lat": 47.579377, + "lon": 19.250221 + }, + { + "lat": 47.579282, + "lon": 19.250328 + }, + { + "lat": 47.578544, + "lon": 19.251735 + }, + { + "lat": 47.578453, + "lon": 19.251871 + }, + { + "lat": 47.578355, + "lon": 19.251952 + }, + { + "lat": 47.578242, + "lon": 19.252004 + }, + { + "lat": 47.578125, + "lon": 19.252021 + }, + { + "lat": 47.578010, + "lon": 19.252007 + }, + { + "lat": 47.577888, + "lon": 19.251951 + }, + { + "lat": 47.577791, + "lon": 19.251865 + }, + { + "lat": 47.577713, + "lon": 19.251753 + }, + { + "lat": 47.577633, + "lon": 19.251594 + }, + { + "lat": 47.577586, + "lon": 19.251417 + }, + { + "lat": 47.577565, + "lon": 19.251250 + }, + { + "lat": 47.577570, + "lon": 19.251089 + }, + { + "lat": 47.577611, + "lon": 19.250849 + }, + { + "lat": 47.577679, + "lon": 19.250659 + }, + { + "lat": 47.577711, + "lon": 19.250593 + } + ] + }, + { + "trackName": "Autodromo Internazionale del Mugello", + "totalLength": 5.242, + "country": "Italy", + "type": "Circuit", + "coordinates": [ + { + "lat": 43.998810, + "lon": 11.372260 + }, + { + "lat": 43.999051, + "lon": 11.372439 + }, + { + "lat": 43.999443, + "lon": 11.372741 + }, + { + "lat": 43.999952, + "lon": 11.373170 + }, + { + "lat": 44.000494, + "lon": 11.373604 + }, + { + "lat": 44.001090, + "lon": 11.374012 + }, + { + "lat": 44.001304, + "lon": 11.374143 + }, + { + "lat": 44.001636, + "lon": 11.374331 + }, + { + "lat": 44.002137, + "lon": 11.374612 + }, + { + "lat": 44.002554, + "lon": 11.374832 + }, + { + "lat": 44.002754, + "lon": 11.374934 + }, + { + "lat": 44.002862, + "lon": 11.374989 + }, + { + "lat": 44.002966, + "lon": 11.375049 + }, + { + "lat": 44.003079, + "lon": 11.375145 + }, + { + "lat": 44.003171, + "lon": 11.375273 + }, + { + "lat": 44.003238, + "lon": 11.375446 + }, + { + "lat": 44.003269, + "lon": 11.375607 + }, + { + "lat": 44.003264, + "lon": 11.375790 + }, + { + "lat": 44.003224, + "lon": 11.375946 + }, + { + "lat": 44.003158, + "lon": 11.376077 + }, + { + "lat": 44.003077, + "lon": 11.376184 + }, + { + "lat": 44.002971, + "lon": 11.376270 + }, + { + "lat": 44.002862, + "lon": 11.376314 + }, + { + "lat": 44.002752, + "lon": 11.376337 + }, + { + "lat": 44.002634, + "lon": 11.376337 + }, + { + "lat": 44.002510, + "lon": 11.376320 + }, + { + "lat": 44.001697, + "lon": 11.376095 + }, + { + "lat": 44.001576, + "lon": 11.376065 + }, + { + "lat": 44.001455, + "lon": 11.376066 + }, + { + "lat": 44.001335, + "lon": 11.376123 + }, + { + "lat": 44.001242, + "lon": 11.376217 + }, + { + "lat": 44.001172, + "lon": 11.376339 + }, + { + "lat": 44.000933, + "lon": 11.376902 + }, + { + "lat": 44.000865, + "lon": 11.377025 + }, + { + "lat": 44.000764, + "lon": 11.377158 + }, + { + "lat": 44.000658, + "lon": 11.377237 + }, + { + "lat": 44.000535, + "lon": 11.377287 + }, + { + "lat": 44.000417, + "lon": 11.377299 + }, + { + "lat": 44.000297, + "lon": 11.377277 + }, + { + "lat": 44.000165, + "lon": 11.377218 + }, + { + "lat": 43.998954, + "lon": 11.376096 + }, + { + "lat": 43.998430, + "lon": 11.375615 + }, + { + "lat": 43.998059, + "lon": 11.375271 + }, + { + "lat": 43.997757, + "lon": 11.374993 + }, + { + "lat": 43.997661, + "lon": 11.374909 + }, + { + "lat": 43.997564, + "lon": 11.374837 + }, + { + "lat": 43.997443, + "lon": 11.374797 + }, + { + "lat": 43.997328, + "lon": 11.374809 + }, + { + "lat": 43.997222, + "lon": 11.374856 + }, + { + "lat": 43.996805, + "lon": 11.375143 + }, + { + "lat": 43.996685, + "lon": 11.375192 + }, + { + "lat": 43.996564, + "lon": 11.375202 + }, + { + "lat": 43.996453, + "lon": 11.375181 + }, + { + "lat": 43.996339, + "lon": 11.375111 + }, + { + "lat": 43.996227, + "lon": 11.375003 + }, + { + "lat": 43.995306, + "lon": 11.373958 + }, + { + "lat": 43.994428, + "lon": 11.372948 + }, + { + "lat": 43.994319, + "lon": 11.372827 + }, + { + "lat": 43.994231, + "lon": 11.372725 + }, + { + "lat": 43.994154, + "lon": 11.372590 + }, + { + "lat": 43.994104, + "lon": 11.372439 + }, + { + "lat": 43.994079, + "lon": 11.372278 + }, + { + "lat": 43.994076, + "lon": 11.372100 + }, + { + "lat": 43.994093, + "lon": 11.371371 + }, + { + "lat": 43.994089, + "lon": 11.371198 + }, + { + "lat": 43.994076, + "lon": 11.371019 + }, + { + "lat": 43.994053, + "lon": 11.370862 + }, + { + "lat": 43.994011, + "lon": 11.370709 + }, + { + "lat": 43.993957, + "lon": 11.370571 + }, + { + "lat": 43.993893, + "lon": 11.370441 + }, + { + "lat": 43.993811, + "lon": 11.370328 + }, + { + "lat": 43.993718, + "lon": 11.370234 + }, + { + "lat": 43.993617, + "lon": 11.370152 + }, + { + "lat": 43.993376, + "lon": 11.370003 + }, + { + "lat": 43.993220, + "lon": 11.369930 + }, + { + "lat": 43.993031, + "lon": 11.369846 + }, + { + "lat": 43.992887, + "lon": 11.369779 + }, + { + "lat": 43.992486, + "lon": 11.369587 + }, + { + "lat": 43.992361, + "lon": 11.369528 + }, + { + "lat": 43.992174, + "lon": 11.369422 + }, + { + "lat": 43.992051, + "lon": 11.369350 + }, + { + "lat": 43.991930, + "lon": 11.369240 + }, + { + "lat": 43.991844, + "lon": 11.369139 + }, + { + "lat": 43.991761, + "lon": 11.369020 + }, + { + "lat": 43.991694, + "lon": 11.368884 + }, + { + "lat": 43.991642, + "lon": 11.368750 + }, + { + "lat": 43.991591, + "lon": 11.368599 + }, + { + "lat": 43.991558, + "lon": 11.368445 + }, + { + "lat": 43.991543, + "lon": 11.368281 + }, + { + "lat": 43.991542, + "lon": 11.368109 + }, + { + "lat": 43.991553, + "lon": 11.367954 + }, + { + "lat": 43.991576, + "lon": 11.367794 + }, + { + "lat": 43.991625, + "lon": 11.367576 + }, + { + "lat": 43.991671, + "lon": 11.367404 + }, + { + "lat": 43.991729, + "lon": 11.367227 + }, + { + "lat": 43.991790, + "lon": 11.367056 + }, + { + "lat": 43.991844, + "lon": 11.366907 + }, + { + "lat": 43.991898, + "lon": 11.366771 + }, + { + "lat": 43.991962, + "lon": 11.366602 + }, + { + "lat": 43.992033, + "lon": 11.366426 + }, + { + "lat": 43.992107, + "lon": 11.366262 + }, + { + "lat": 43.992186, + "lon": 11.366124 + }, + { + "lat": 43.992277, + "lon": 11.366008 + }, + { + "lat": 43.992393, + "lon": 11.365908 + }, + { + "lat": 43.992513, + "lon": 11.365840 + }, + { + "lat": 43.992626, + "lon": 11.365801 + }, + { + "lat": 43.992754, + "lon": 11.365784 + }, + { + "lat": 43.992911, + "lon": 11.365787 + }, + { + "lat": 43.993024, + "lon": 11.365802 + }, + { + "lat": 43.993159, + "lon": 11.365830 + }, + { + "lat": 43.994536, + "lon": 11.366200 + }, + { + "lat": 43.994804, + "lon": 11.366269 + }, + { + "lat": 43.995315, + "lon": 11.366408 + }, + { + "lat": 43.995422, + "lon": 11.366445 + }, + { + "lat": 43.995526, + "lon": 11.366515 + }, + { + "lat": 43.995608, + "lon": 11.366628 + }, + { + "lat": 43.995678, + "lon": 11.366780 + }, + { + "lat": 43.995713, + "lon": 11.366947 + }, + { + "lat": 43.995723, + "lon": 11.367662 + }, + { + "lat": 43.995732, + "lon": 11.367824 + }, + { + "lat": 43.995766, + "lon": 11.367981 + }, + { + "lat": 43.995829, + "lon": 11.368128 + }, + { + "lat": 43.995909, + "lon": 11.368249 + }, + { + "lat": 43.996011, + "lon": 11.368349 + }, + { + "lat": 43.996128, + "lon": 11.368414 + }, + { + "lat": 43.997342, + "lon": 11.368898 + }, + { + "lat": 43.997571, + "lon": 11.368989 + }, + { + "lat": 43.997724, + "lon": 11.369051 + }, + { + "lat": 43.998154, + "lon": 11.369224 + }, + { + "lat": 43.998579, + "lon": 11.369393 + }, + { + "lat": 43.998779, + "lon": 11.369473 + }, + { + "lat": 43.998911, + "lon": 11.369539 + }, + { + "lat": 43.999020, + "lon": 11.369617 + }, + { + "lat": 43.999112, + "lon": 11.369711 + }, + { + "lat": 43.999192, + "lon": 11.369832 + }, + { + "lat": 43.999255, + "lon": 11.369985 + }, + { + "lat": 43.999288, + "lon": 11.370139 + }, + { + "lat": 43.999301, + "lon": 11.370291 + }, + { + "lat": 43.999291, + "lon": 11.370446 + }, + { + "lat": 43.999245, + "lon": 11.370610 + }, + { + "lat": 43.999176, + "lon": 11.370749 + }, + { + "lat": 43.999093, + "lon": 11.370863 + }, + { + "lat": 43.998995, + "lon": 11.370944 + }, + { + "lat": 43.998858, + "lon": 11.371008 + }, + { + "lat": 43.998746, + "lon": 11.371022 + }, + { + "lat": 43.998632, + "lon": 11.371010 + }, + { + "lat": 43.998521, + "lon": 11.370970 + }, + { + "lat": 43.998411, + "lon": 11.370906 + }, + { + "lat": 43.998296, + "lon": 11.370819 + }, + { + "lat": 43.998195, + "lon": 11.370736 + }, + { + "lat": 43.997671, + "lon": 11.370167 + }, + { + "lat": 43.997503, + "lon": 11.369985 + }, + { + "lat": 43.997390, + "lon": 11.369885 + }, + { + "lat": 43.997274, + "lon": 11.369840 + }, + { + "lat": 43.997150, + "lon": 11.369829 + }, + { + "lat": 43.996799, + "lon": 11.369827 + }, + { + "lat": 43.996671, + "lon": 11.369830 + }, + { + "lat": 43.996564, + "lon": 11.369812 + }, + { + "lat": 43.996460, + "lon": 11.369769 + }, + { + "lat": 43.996355, + "lon": 11.369687 + }, + { + "lat": 43.996188, + "lon": 11.369512 + }, + { + "lat": 43.995854, + "lon": 11.369167 + }, + { + "lat": 43.994846, + "lon": 11.368128 + }, + { + "lat": 43.994713, + "lon": 11.367991 + }, + { + "lat": 43.994499, + "lon": 11.367770 + }, + { + "lat": 43.994127, + "lon": 11.367388 + }, + { + "lat": 43.993758, + "lon": 11.367005 + }, + { + "lat": 43.993579, + "lon": 11.366834 + }, + { + "lat": 43.993469, + "lon": 11.366744 + }, + { + "lat": 43.993342, + "lon": 11.366660 + }, + { + "lat": 43.993216, + "lon": 11.366610 + }, + { + "lat": 43.993104, + "lon": 11.366587 + }, + { + "lat": 43.992985, + "lon": 11.366592 + }, + { + "lat": 43.992876, + "lon": 11.366627 + }, + { + "lat": 43.992756, + "lon": 11.366704 + }, + { + "lat": 43.992653, + "lon": 11.366796 + }, + { + "lat": 43.992560, + "lon": 11.366940 + }, + { + "lat": 43.992494, + "lon": 11.367102 + }, + { + "lat": 43.992451, + "lon": 11.367273 + }, + { + "lat": 43.992437, + "lon": 11.367452 + }, + { + "lat": 43.992452, + "lon": 11.367648 + }, + { + "lat": 43.992486, + "lon": 11.367812 + }, + { + "lat": 43.992533, + "lon": 11.367959 + }, + { + "lat": 43.992612, + "lon": 11.368121 + }, + { + "lat": 43.992698, + "lon": 11.368262 + }, + { + "lat": 43.992788, + "lon": 11.368379 + }, + { + "lat": 43.992894, + "lon": 11.368495 + }, + { + "lat": 43.993008, + "lon": 11.368599 + }, + { + "lat": 43.993116, + "lon": 11.368678 + }, + { + "lat": 43.993324, + "lon": 11.368818 + }, + { + "lat": 43.993565, + "lon": 11.368965 + }, + { + "lat": 43.993808, + "lon": 11.369112 + }, + { + "lat": 43.993953, + "lon": 11.369205 + }, + { + "lat": 43.994230, + "lon": 11.369389 + }, + { + "lat": 43.997594, + "lon": 11.371481 + }, + { + "lat": 43.998810, + "lon": 11.372260 + } + ] + }, + { + "trackName": "Circuit Paul Ricard (GP layout)", + "totalLength": 5.856, + "country": "France", + "type": "Circuit", + "coordinates": [ + { + "lat": 43.249575, + "lon": 5.802889 + }, + { + "lat": 43.249715, + "lon": 5.802880 + }, + { + "lat": 43.249835, + "lon": 5.802861 + }, + { + "lat": 43.249940, + "lon": 5.802810 + }, + { + "lat": 43.250062, + "lon": 5.802701 + }, + { + "lat": 43.250135, + "lon": 5.802591 + }, + { + "lat": 43.250178, + "lon": 5.802450 + }, + { + "lat": 43.250189, + "lon": 5.802269 + }, + { + "lat": 43.250150, + "lon": 5.802076 + }, + { + "lat": 43.250095, + "lon": 5.801932 + }, + { + "lat": 43.249575, + "lon": 5.802889 + }, + { + "lat": 43.249449, + "lon": 5.802847 + }, + { + "lat": 43.249315, + "lon": 5.802769 + }, + { + "lat": 43.249191, + "lon": 5.802668 + }, + { + "lat": 43.249067, + "lon": 5.802536 + }, + { + "lat": 43.248528, + "lon": 5.801987 + }, + { + "lat": 43.248431, + "lon": 5.801883 + }, + { + "lat": 43.248254, + "lon": 5.801728 + }, + { + "lat": 43.248146, + "lon": 5.801640 + }, + { + "lat": 43.248046, + "lon": 5.801563 + }, + { + "lat": 43.247893, + "lon": 5.801464 + }, + { + "lat": 43.247763, + "lon": 5.801407 + }, + { + "lat": 43.247632, + "lon": 5.801388 + }, + { + "lat": 43.247483, + "lon": 5.801443 + }, + { + "lat": 43.247384, + "lon": 5.801534 + }, + { + "lat": 43.247293, + "lon": 5.801642 + }, + { + "lat": 43.247178, + "lon": 5.801838 + }, + { + "lat": 43.247095, + "lon": 5.802047 + }, + { + "lat": 43.247050, + "lon": 5.802250 + }, + { + "lat": 43.247032, + "lon": 5.802492 + }, + { + "lat": 43.247050, + "lon": 5.802727 + }, + { + "lat": 43.247088, + "lon": 5.802929 + }, + { + "lat": 43.247151, + "lon": 5.803113 + }, + { + "lat": 43.247233, + "lon": 5.803264 + }, + { + "lat": 43.247307, + "lon": 5.803373 + }, + { + "lat": 43.247429, + "lon": 5.803499 + }, + { + "lat": 43.247543, + "lon": 5.803586 + }, + { + "lat": 43.247643, + "lon": 5.803647 + }, + { + "lat": 43.247755, + "lon": 5.803682 + }, + { + "lat": 43.247905, + "lon": 5.803707 + }, + { + "lat": 43.248017, + "lon": 5.803708 + }, + { + "lat": 43.249247, + "lon": 5.803721 + }, + { + "lat": 43.249814, + "lon": 5.803715 + }, + { + "lat": 43.249934, + "lon": 5.803707 + }, + { + "lat": 43.250049, + "lon": 5.803692 + }, + { + "lat": 43.250175, + "lon": 5.803661 + }, + { + "lat": 43.250312, + "lon": 5.803613 + }, + { + "lat": 43.250447, + "lon": 5.803554 + }, + { + "lat": 43.250573, + "lon": 5.803483 + }, + { + "lat": 43.250672, + "lon": 5.803400 + }, + { + "lat": 43.250762, + "lon": 5.803301 + }, + { + "lat": 43.250881, + "lon": 5.803151 + }, + { + "lat": 43.250961, + "lon": 5.803043 + }, + { + "lat": 43.251041, + "lon": 5.802924 + }, + { + "lat": 43.251106, + "lon": 5.802798 + }, + { + "lat": 43.251176, + "lon": 5.802606 + }, + { + "lat": 43.252924, + "lon": 5.796206 + }, + { + "lat": 43.253775, + "lon": 5.793063 + }, + { + "lat": 43.253963, + "lon": 5.792377 + }, + { + "lat": 43.254138, + "lon": 5.791720 + }, + { + "lat": 43.254734, + "lon": 5.789542 + }, + { + "lat": 43.254857, + "lon": 5.789090 + }, + { + "lat": 43.254963, + "lon": 5.788706 + }, + { + "lat": 43.255437, + "lon": 5.786961 + }, + { + "lat": 43.256144, + "lon": 5.784355 + }, + { + "lat": 43.256291, + "lon": 5.783813 + }, + { + "lat": 43.257018, + "lon": 5.781153 + }, + { + "lat": 43.257084, + "lon": 5.780938 + }, + { + "lat": 43.257148, + "lon": 5.780794 + }, + { + "lat": 43.257247, + "lon": 5.780664 + }, + { + "lat": 43.257362, + "lon": 5.780579 + }, + { + "lat": 43.257544, + "lon": 5.780472 + }, + { + "lat": 43.257677, + "lon": 5.780397 + }, + { + "lat": 43.257807, + "lon": 5.780322 + }, + { + "lat": 43.257949, + "lon": 5.780220 + }, + { + "lat": 43.258077, + "lon": 5.780113 + }, + { + "lat": 43.258166, + "lon": 5.780025 + }, + { + "lat": 43.258256, + "lon": 5.779916 + }, + { + "lat": 43.258339, + "lon": 5.779799 + }, + { + "lat": 43.258470, + "lon": 5.779573 + }, + { + "lat": 43.258563, + "lon": 5.779368 + }, + { + "lat": 43.258614, + "lon": 5.779217 + }, + { + "lat": 43.258644, + "lon": 5.779065 + }, + { + "lat": 43.258633, + "lon": 5.778845 + }, + { + "lat": 43.258566, + "lon": 5.778637 + }, + { + "lat": 43.258489, + "lon": 5.778521 + }, + { + "lat": 43.258353, + "lon": 5.778430 + }, + { + "lat": 43.258098, + "lon": 5.778307 + }, + { + "lat": 43.257812, + "lon": 5.778172 + }, + { + "lat": 43.257704, + "lon": 5.778120 + }, + { + "lat": 43.257599, + "lon": 5.778183 + }, + { + "lat": 43.257582, + "lon": 5.778379 + }, + { + "lat": 43.257635, + "lon": 5.779026 + }, + { + "lat": 43.257632, + "lon": 5.779216 + }, + { + "lat": 43.257596, + "lon": 5.779398 + }, + { + "lat": 43.257534, + "lon": 5.779533 + }, + { + "lat": 43.257453, + "lon": 5.779643 + }, + { + "lat": 43.257330, + "lon": 5.779724 + }, + { + "lat": 43.257210, + "lon": 5.779741 + }, + { + "lat": 43.256983, + "lon": 5.779697 + }, + { + "lat": 43.256873, + "lon": 5.779686 + }, + { + "lat": 43.256747, + "lon": 5.779726 + }, + { + "lat": 43.256653, + "lon": 5.779825 + }, + { + "lat": 43.256554, + "lon": 5.780019 + }, + { + "lat": 43.256459, + "lon": 5.780233 + }, + { + "lat": 43.256363, + "lon": 5.780452 + }, + { + "lat": 43.256284, + "lon": 5.780638 + }, + { + "lat": 43.256196, + "lon": 5.780861 + }, + { + "lat": 43.256110, + "lon": 5.781088 + }, + { + "lat": 43.256042, + "lon": 5.781295 + }, + { + "lat": 43.255987, + "lon": 5.781481 + }, + { + "lat": 43.255943, + "lon": 5.781641 + }, + { + "lat": 43.255878, + "lon": 5.781877 + }, + { + "lat": 43.255819, + "lon": 5.782053 + }, + { + "lat": 43.255750, + "lon": 5.782224 + }, + { + "lat": 43.255684, + "lon": 5.782384 + }, + { + "lat": 43.255595, + "lon": 5.782578 + }, + { + "lat": 43.255479, + "lon": 5.782808 + }, + { + "lat": 43.255110, + "lon": 5.783474 + }, + { + "lat": 43.254996, + "lon": 5.783679 + }, + { + "lat": 43.254880, + "lon": 5.783866 + }, + { + "lat": 43.254760, + "lon": 5.784030 + }, + { + "lat": 43.254624, + "lon": 5.784212 + }, + { + "lat": 43.254509, + "lon": 5.784396 + }, + { + "lat": 43.254420, + "lon": 5.784603 + }, + { + "lat": 43.254177, + "lon": 5.785243 + }, + { + "lat": 43.254131, + "lon": 5.785422 + }, + { + "lat": 43.254130, + "lon": 5.785651 + }, + { + "lat": 43.254167, + "lon": 5.785843 + }, + { + "lat": 43.254237, + "lon": 5.786002 + }, + { + "lat": 43.254537, + "lon": 5.786549 + }, + { + "lat": 43.254573, + "lon": 5.786750 + }, + { + "lat": 43.254556, + "lon": 5.786901 + }, + { + "lat": 43.254462, + "lon": 5.787130 + }, + { + "lat": 43.254219, + "lon": 5.787572 + }, + { + "lat": 43.253883, + "lon": 5.788173 + }, + { + "lat": 43.253112, + "lon": 5.789564 + }, + { + "lat": 43.251287, + "lon": 5.792878 + }, + { + "lat": 43.251187, + "lon": 5.793059 + }, + { + "lat": 43.250004, + "lon": 5.795165 + }, + { + "lat": 43.249693, + "lon": 5.795739 + }, + { + "lat": 43.249551, + "lon": 5.795994 + }, + { + "lat": 43.249493, + "lon": 5.796134 + }, + { + "lat": 43.249511, + "lon": 5.796280 + }, + { + "lat": 43.249627, + "lon": 5.796353 + }, + { + "lat": 43.249839, + "lon": 5.796364 + }, + { + "lat": 43.250200, + "lon": 5.796370 + }, + { + "lat": 43.250319, + "lon": 5.796382 + }, + { + "lat": 43.250447, + "lon": 5.796461 + }, + { + "lat": 43.250535, + "lon": 5.796553 + }, + { + "lat": 43.250625, + "lon": 5.796706 + }, + { + "lat": 43.250698, + "lon": 5.796886 + }, + { + "lat": 43.250738, + "lon": 5.797030 + }, + { + "lat": 43.250754, + "lon": 5.797207 + }, + { + "lat": 43.250750, + "lon": 5.797391 + }, + { + "lat": 43.250710, + "lon": 5.797607 + }, + { + "lat": 43.250627, + "lon": 5.797809 + }, + { + "lat": 43.250544, + "lon": 5.797942 + }, + { + "lat": 43.250448, + "lon": 5.798069 + }, + { + "lat": 43.250329, + "lon": 5.798229 + }, + { + "lat": 43.250255, + "lon": 5.798341 + }, + { + "lat": 43.250178, + "lon": 5.798461 + }, + { + "lat": 43.250108, + "lon": 5.798582 + }, + { + "lat": 43.250044, + "lon": 5.798706 + }, + { + "lat": 43.249950, + "lon": 5.798930 + }, + { + "lat": 43.249867, + "lon": 5.799181 + }, + { + "lat": 43.249809, + "lon": 5.799422 + }, + { + "lat": 43.249769, + "lon": 5.799657 + }, + { + "lat": 43.249744, + "lon": 5.799874 + }, + { + "lat": 43.249733, + "lon": 5.800100 + }, + { + "lat": 43.249735, + "lon": 5.800359 + }, + { + "lat": 43.249750, + "lon": 5.800623 + }, + { + "lat": 43.249780, + "lon": 5.800874 + }, + { + "lat": 43.249816, + "lon": 5.801096 + }, + { + "lat": 43.249860, + "lon": 5.801291 + }, + { + "lat": 43.249921, + "lon": 5.801496 + }, + { + "lat": 43.250018, + "lon": 5.801756 + }, + { + "lat": 43.250095, + "lon": 5.801932 + } + ] } -] \ No newline at end of file +] From debcab31ef22136d2efcd99ac60ac76a665614fc Mon Sep 17 00:00:00 2001 From: "meszaros.martin986@gmail.com" Date: Fri, 31 Jul 2026 15:17:19 +0200 Subject: [PATCH 5/5] Refactored and standardized UI components across the application using a new centralized theme and component system. This update replaces hand-rolled styling and hardcoded layouts with reusable components like `AppTopBar`, `StatCell`, `AppCard`, and `SectionLabel`, ensuring visual consistency and improved readability across all screens. **Key Changes:** * **Global Theme & Styling:** * Updated `DarkTrackProColors` and `LightTrackProColors` with refined, desaturated accent colors and improved semantic color names (e.g., `danger`, `textFaint`). * Integrated centralized `Spacing`, `TrackProShapes`, and `TrackProType` (typography) definitions across all screens. * Standardized data visualization colors (gauges, charts, map markers) in a new `DataVizColors` object. * **Component Migration:** * Replaced custom top bars with the `AppTopBar` component in `TrackScreen`, `TimeAttackListView`, `TrackListView`, `SettingsScreen`, and all list item details. * Standardized statistics display using `StatCell` and `StatCellDivider`, replacing custom column/row implementations in vehicle, track, and session detail screens. * Migrated specific UI patterns to `EmptyState`, `ToggleChip`, and `ExpandableGroup` components. * Introduced `AppDropdownField` as a generic replacement for specialized dropdown implementations. * Unified button styling using a new `PrimaryButton` component. * **Screen-Specific Improvements:** * **`TrackScreen.kt` & `TrackBuilderScreen.kt`:** Standardized layout spacing and replaced manual borders/shapes with theme-defined values. Updated MapLibre colors to use `DataVizColors`. * **`DragScreen.kt` & `DragScreenListItem.kt`:** Overhauled the speed gauge and telemetry charts to use theme-consistent colors. Refactored the metric grid using `StatCell`. * **`LapDetailScreen.kt`:** Improved the heatmap mode switcher styling and unified the comparison statistics panel. * **`CarViewScreen.kt` & `CarCreatorScreen.kt`:** Standardized performance stat grids and form fields. * **`SettingsScreen.kt`:** Simplified the settings layout using a unified `SettingsToggleRow`. * **UI/UX Refinement:** * Replaced all-caps text with sentence-case in several labels for better readability. * Adjusted icon sizes and padding across the main dashboard drawer and action cards for a cleaner look. * Updated delete confirmation dialogs to use standardized typography and danger colors. --- .../java/com/example/trackpro/MainActivity.kt | 228 ++++++----- .../trackpro/extrasForUI/CustomTextField.kt | 12 +- .../trackpro/extrasForUI/DropDownMenuField.kt | 113 +++--- .../screens/ESPConnectionTestScreen.kt | 180 ++++----- .../trackpro/screens/SettingsScreen.kt | 242 ++++-------- .../trackpro/screens/TrackBuilderScreen.kt | 173 ++++----- .../example/trackpro/screens/TrackScreen.kt | 147 +++----- .../screens/TrackVehicleSelectorScreen.kt | 148 ++++---- .../screens/listViewScreens/CarListView.kt | 204 +++------- .../screens/listViewScreens/DragTimesList.kt | 173 +++------ .../listViewScreens/TimeAttackListView.kt | 166 +++----- .../screens/listViewScreens/TrackListView.kt | 149 +++----- .../listItems/CarViewScreen.kt | 154 +++----- .../listItems/DragScreenListItem.kt | 102 ++--- .../listItems/LapDetailScreen.kt | 201 ++++------ .../listItems/TimeAttackListItem.kt | 225 +++++------ .../screens/telemetricScreens/DragScreen.kt | 355 +++++++----------- .../telemetricScreens/TimeAttackScreen.kt | 306 +++++---------- .../vehicleScreens/CarCreatorScreen.kt | 189 ++++------ .../example/trackpro/theme/TrackProColors.kt | 57 +-- 20 files changed, 1301 insertions(+), 2223 deletions(-) diff --git a/app/src/main/java/com/example/trackpro/MainActivity.kt b/app/src/main/java/com/example/trackpro/MainActivity.kt index 9bcb9ed..f870530 100644 --- a/app/src/main/java/com/example/trackpro/MainActivity.kt +++ b/app/src/main/java/com/example/trackpro/MainActivity.kt @@ -9,7 +9,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -23,11 +22,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CarRepair import androidx.compose.material.icons.filled.FlagCircle +import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.RocketLaunch import androidx.compose.material.icons.filled.Settings @@ -49,7 +50,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -92,6 +92,11 @@ import com.example.trackpro.viewModels.VehicleFULLViewModel import com.example.trackpro.viewModels.VehicleFULLViewModelFactory import com.example.trackpro.viewModels.VehicleViewModel import com.example.trackpro.viewModels.VehicleViewModelFactory +import com.example.trackpro.components.AppCard +import com.example.trackpro.components.SectionLabel +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -325,27 +330,32 @@ fun MainScreen( Box( modifier = Modifier .fillMaxWidth() - .background(TrackProTheme.colors.accentCyan) - .padding(horizontal = 24.dp, vertical = 20.dp) + .background(TrackProTheme.colors.bgElevated) + .padding(horizontal = 20.dp, vertical = 18.dp) ) { - Column { - Text( - text = "TRACKPRO", - color = Color.Black, - fontSize = 22.sp, - fontWeight = FontWeight.Black, - letterSpacing = 4.sp - ) - Text( - text = "Performance Telemetry", - color = Color.Black.copy(alpha = 0.7f), - fontSize = 11.sp, - letterSpacing = 1.sp + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(6.dp) + .background(TrackProTheme.colors.accentCyan, CircleShape) ) + Spacer(Modifier.width(10.dp)) + Column { + Text( + text = "TRACKPRO", + style = TrackProType.titleLarge, + color = TrackProTheme.colors.textPrimary + ) + Text( + text = "Performance Telemetry", + style = TrackProType.body.copy(fontSize = 11.sp), + color = TrackProTheme.colors.textFaint + ) + } } } - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(4.dp)) DrawerSection(title = "SESSIONS") { DrawerItem( @@ -418,7 +428,7 @@ fun MainScreen( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 8.dp, vertical = 4.dp) + .padding(horizontal = 4.dp, vertical = 2.dp) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -431,13 +441,11 @@ fun MainScreen( } Text( text = "TRACKPRO", - color = TrackProTheme.colors.accentCyan, - fontSize = 14.sp, - fontWeight = FontWeight.Black, - letterSpacing = 4.sp + style = TrackProType.label.copy(fontSize = 13.sp, letterSpacing = 2.sp), + color = TrackProTheme.colors.textPrimary ) // Spacer to balance the row - Box(modifier = Modifier.size(48.dp)) + Box(modifier = Modifier.size(40.dp)) } } @@ -448,31 +456,26 @@ fun MainScreen( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 28.dp, vertical = 32.dp) + .padding(horizontal = Spacing.lg, vertical = Spacing.xl) ) { Column { - // Red accent line Box( modifier = Modifier - .width(40.dp) - .height(3.dp) + .width(28.dp) + .height(2.dp) .background(TrackProTheme.colors.accentCyan) ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(10.dp)) Text( - text = "READY TO\nBEAT RECORDS?", - color = TrackProTheme.colors.textPrimary, - fontSize = 32.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-0.5).sp, - lineHeight = 36.sp + text = "Ready to beat records?", + style = TrackProType.titleLarge, + color = TrackProTheme.colors.textPrimary ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(4.dp)) Text( text = "GPS telemetry · Lap timing · Performance analysis", - color = TrackProTheme.colors.textMuted, - fontSize = 12.sp, - letterSpacing = 0.5.sp + style = TrackProType.body, + color = TrackProTheme.colors.textMuted ) } } @@ -484,8 +487,8 @@ fun MainScreen( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + .padding(Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { // Primary racing actions — full width @@ -597,79 +600,76 @@ private fun ActionCard( disabled: Boolean = false ) { val alpha = if (disabled) 0.4f else 1f - val iconSize = if (halfWidth) 16.dp else if (fullWidth) 22.dp else 18.dp - val iconBoxSize = if (halfWidth) 32.dp else if (fullWidth) 44.dp else 36.dp - val titleSize = if (halfWidth) 11.sp else if (fullWidth) 14.sp else 12.sp + val iconSize = if (halfWidth) 14.dp else if (fullWidth) 18.dp else 16.dp + val iconBoxSize = if (halfWidth) 26.dp else if (fullWidth) 34.dp else 30.dp + val titleStyle = if (fullWidth) TrackProType.titleMedium else TrackProType.titleMedium.copy(fontSize = 13.sp) val subtitleSize = if (halfWidth) 9.sp else 10.sp - val vertPadding = if (halfWidth) 14.dp else if (fullWidth) 18.dp else 14.dp + val vertPadding = if (halfWidth) 10.dp else if (fullWidth) 12.dp else 10.dp - Box( + AppCard( modifier = Modifier .fillMaxWidth() - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(10.dp)) - .border(1.dp, TrackProTheme.colors.sectorLine, RoundedCornerShape(10.dp)) - .then(if (!disabled) Modifier.clickable(onClick = onClick) else Modifier) + .then(if (!disabled) Modifier.clickable(onClick = onClick) else Modifier), + padding = 0.dp ) { - // Left accent bar - Box( - modifier = Modifier - .align(Alignment.CenterStart) - .width(3.dp) - .height(48.dp) - .background( - accentColor.copy(alpha = alpha), - RoundedCornerShape(topEnd = 2.dp, bottomEnd = 2.dp) - ) - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = vertPadding), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { + Box { + // Left accent bar — the accent's only job on this card Box( modifier = Modifier - .size(iconBoxSize) - .background(accentColor.copy(alpha = 0.1f * alpha), RoundedCornerShape(8.dp)), - contentAlignment = Alignment.Center + .align(Alignment.CenterStart) + .width(2.dp) + .height(36.dp) + .background( + accentColor.copy(alpha = alpha), + RoundedCornerShape(topEnd = 2.dp, bottomEnd = 2.dp) + ) + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Spacing.md, vertical = vertPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { - Icon( - imageVector = icon, - contentDescription = title, - tint = accentColor.copy(alpha = alpha), - modifier = Modifier.size(iconSize) - ) - } + Box( + modifier = Modifier + .size(iconBoxSize) + .background(accentColor.copy(alpha = 0.12f * alpha), TrackProShapes.control), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = title, + tint = accentColor.copy(alpha = alpha), + modifier = Modifier.size(iconSize) + ) + } - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - color = TrackProTheme.colors.textPrimary.copy(alpha = alpha), - fontSize = titleSize, - fontWeight = FontWeight.Black, - letterSpacing = 0.5.sp, // reduced from 1.sp - lineHeight = (titleSize.value + 2).sp, - softWrap = true - ) - Text( - text = subtitle, - color = TrackProTheme.colors.textMuted.copy(alpha = alpha), - fontSize = subtitleSize, - letterSpacing = 0.sp, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = titleStyle, + color = TrackProTheme.colors.textPrimary.copy(alpha = alpha), + softWrap = true + ) + Text( + text = subtitle, + style = TrackProType.body.copy(fontSize = subtitleSize), + color = TrackProTheme.colors.textMuted.copy(alpha = alpha), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } - if (!disabled && !halfWidth) { - Text( - text = "→", - color = accentColor.copy(alpha = 0.6f), - fontSize = 14.sp, - fontWeight = FontWeight.Black - ) + if (!disabled && !halfWidth) { + Icon( + imageVector = Icons.Default.KeyboardArrowRight, + contentDescription = null, + tint = accentColor.copy(alpha = 0.6f), + modifier = Modifier.size(16.dp) + ) + } } } } @@ -679,13 +679,9 @@ private fun ActionCard( @Composable private fun DrawerSection(title: String, content: @Composable () -> Unit) { - Text( + SectionLabel( text = title, - color = TrackProTheme.colors.textMuted, - fontSize = 9.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp, - modifier = Modifier.padding(start = 24.dp, top = 16.dp, bottom = 6.dp) + modifier = Modifier.padding(start = 20.dp, top = 14.dp, bottom = 4.dp) ) content() } @@ -701,19 +697,19 @@ private fun DrawerItem( modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick) - .padding(horizontal = 24.dp, vertical = 12.dp), + .padding(horizontal = 20.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(14.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Box( modifier = Modifier - .size(32.dp) - .background(tint.copy(alpha = 0.1f), RoundedCornerShape(6.dp)), + .size(28.dp) + .background(tint.copy(alpha = 0.12f), TrackProShapes.control), contentAlignment = Alignment.Center ) { - Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(16.dp)) + Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(15.dp)) } - Text(label, color = TrackProTheme.colors.textPrimary, fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(label, style = TrackProType.body, color = TrackProTheme.colors.textPrimary) } } 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 7b4d733..79871e9 100644 --- a/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt +++ b/app/src/main/java/com/example/trackpro/extrasForUI/CustomTextField.kt @@ -5,8 +5,10 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.input.KeyboardType +import com.example.trackpro.theme.TrackProType @Composable fun CustomTextField( @@ -14,12 +16,14 @@ fun CustomTextField( value: String, isNumber: Boolean = false, leadingIcon: ImageVector? = null, + accent: Color = TrackProTheme.colors.accentCyan, onValueChange: (String) -> Unit ) { OutlinedTextField( value = value, onValueChange = onValueChange, - label = { Text(label, color = TrackProTheme.colors.textMuted) }, + textStyle = TrackProType.body, + label = { Text(label, style = TrackProType.body, color = TrackProTheme.colors.textMuted) }, keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default, leadingIcon = if (leadingIcon != null) { { @@ -33,10 +37,10 @@ fun CustomTextField( colors = OutlinedTextFieldDefaults.colors( focusedTextColor = TrackProTheme.colors.textPrimary, unfocusedTextColor = TrackProTheme.colors.textPrimary, - focusedBorderColor = TrackProTheme.colors.accentCyan, + focusedBorderColor = accent, unfocusedBorderColor = TrackProTheme.colors.sectorLine, - focusedLabelColor = TrackProTheme.colors.accentCyan, - cursorColor = TrackProTheme.colors.accentCyan + focusedLabelColor = accent, + cursorColor = accent ), modifier = Modifier.fillMaxWidth() ) 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 504cc5c..07a7788 100644 --- a/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt +++ b/app/src/main/java/com/example/trackpro/extrasForUI/DropDownMenuField.kt @@ -22,6 +22,71 @@ import com.example.trackpro.models.VehiclePair +/** + * Generic replacement for [DropdownMenuFieldMulti] / [TrackDropdownMenu] below (originally + * a third, string-only copy existed too - migrated and removed). New call sites should use + * this one; the remaining two are migrated screen-by-screen and then removed. + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun AppDropdownField( + label: String, + items: List, + selectedLabel: String, + itemLabel: (T) -> String, + onSelect: (T) -> Unit, + modifier: Modifier = Modifier, + accent: Color = TrackProTheme.colors.accentCyan, + emptyMessage: String = "No options available" +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = selectedLabel, + onValueChange = {}, + readOnly = true, + label = { Text(label, color = TrackProTheme.colors.textMuted) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = TrackProTheme.colors.textPrimary, + unfocusedTextColor = TrackProTheme.colors.textPrimary, + focusedBorderColor = accent, + unfocusedBorderColor = TrackProTheme.colors.sectorLine + ), + modifier = Modifier.fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.background(TrackProTheme.colors.bgElevated) + ) { + if (items.isEmpty()) { + DropdownMenuItem( + text = { Text(emptyMessage, color = TrackProTheme.colors.textMuted) }, + onClick = { expanded = false }, + enabled = false + ) + } else { + items.forEach { item -> + DropdownMenuItem( + text = { Text(itemLabel(item), color = TrackProTheme.colors.textPrimary) }, + onClick = { + expanded = false + onSelect(item) + } + ) + } + } + } + } +} + @OptIn(ExperimentalMaterialApi::class) @Composable fun DropdownMenuFieldMulti(label: String, options: List, selectedOption: String, onOptionSelected: (Long) -> Unit) { @@ -121,51 +186,3 @@ fun TrackDropdownMenu( -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun DropdownMenuField( - label: String, - options: List, - selectedOption: String, - textColor: Color = TrackProTheme.colors.textPrimary, - onOptionSelected: (String) -> Unit -) { - var expanded by remember { mutableStateOf(false) } - - ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) { - OutlinedTextField( - value = selectedOption, - onValueChange = {}, - readOnly = true, - label = { Text(label, color = TrackProTheme.colors.textMuted) }, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) - }, - colors = OutlinedTextFieldDefaults.colors( - focusedTextColor = textColor, - unfocusedTextColor = textColor, - focusedBorderColor = TrackProTheme.colors.accentCyan, - unfocusedBorderColor = TrackProTheme.colors.sectorLine - ), - modifier = Modifier - .fillMaxWidth() - ) - - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - modifier = Modifier.background(TrackProTheme.colors.bgElevated) - ) { - options.forEach { option -> - DropdownMenuItem( - text = { Text(option, color = TrackProTheme.colors.textPrimary) }, - onClick = { - onOptionSelected(option) - expanded = false - } - ) - } - } - } -} - 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 598ed80..b63c7d7 100644 --- a/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/ESPConnectionTestScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider @@ -34,12 +34,19 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.graphics.toColorInt import com.example.trackpro.TrackProApp import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.SectionLabel +import com.example.trackpro.components.StatCell +import com.example.trackpro.components.StatCellDivider +import com.example.trackpro.components.StatCellSize +import com.example.trackpro.theme.DataVizColors +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 kotlin.math.cos @@ -73,39 +80,18 @@ fun ESPConnectionTestScreen() { ) { Column(modifier = Modifier.fillMaxSize()) { - // ── Top Bar (Reacts to Source) ──────────────── - Box( - modifier = Modifier - .fillMaxWidth() - .background(if (isConnected) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentCyan) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { + AppTopBar( + title = if (useExternal) "ESP32 Mode" else "Phone GPS Mode", + accent = if (isConnected) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentCyan, + trailing = { Text( - text = if (useExternal) "● ESP32 MODE" else "● PHONE GPS MODE", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 2.sp + text = "Switch", + style = TrackProType.label, + color = TrackProTheme.colors.accentCyan, + modifier = Modifier.clickable { app.useExternalGps.value = !useExternal } ) - - // Simple Toggle Switch in the Top Bar - androidx.compose.material3.Button( - onClick = { app.useExternalGps.value = !useExternal }, - colors = androidx.compose.material3.ButtonDefaults.buttonColors( - containerColor = Color.Black.copy(alpha = 0.2f) - ), - contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 12.dp, vertical = 0.dp), - modifier = Modifier.height(24.dp) - ) { - Text("SWITCH", color = Color.Black, fontSize = 9.sp, fontWeight = FontWeight.Bold) - } } - } + ) Column( modifier = Modifier @@ -128,10 +114,8 @@ fun ESPConnectionTestScreen() { Spacer(Modifier.height(8.dp)) Text( text = UnitFormatter.speedUnitLabel(useMetric).lowercase(), - color = TrackProTheme.colors.textMuted, - fontSize = 12.sp, - letterSpacing = 3.sp, - fontWeight = FontWeight.Bold + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) } } @@ -143,52 +127,54 @@ fun ESPConnectionTestScreen() { modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgElevated) - .padding(horizontal = 24.dp, vertical = 14.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.md), horizontalArrangement = Arrangement.SpaceBetween ) { - SignalCell( - label = "SOURCE", - value = if (useExternal) "ESP32" else "INTERNAL", - valueColor = TrackProTheme.colors.textPrimary, - textMuted = TrackProTheme.colors.textMuted + StatCell( + label = "Source", + value = if (useExternal) "ESP32" else "Internal", + size = StatCellSize.Small, + horizontalAlignment = Alignment.CenterHorizontally ) - VerticalDividerLine(TrackProTheme.colors.sectorLine) - SignalCell( - label = "STATUS", - value = if (isConnected) "LIVE" else "OFFLINE", + StatCellDivider() + StatCell( + label = "Status", + value = if (isConnected) "Live" else "Offline", valueColor = if (isConnected) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentCyan, - textMuted = TrackProTheme.colors.textMuted + size = StatCellSize.Small, + horizontalAlignment = Alignment.CenterHorizontally ) - VerticalDividerLine(TrackProTheme.colors.sectorLine) - SignalCell( - label = "FIX", - value = if (fix) "OK" else "WAIT", + StatCellDivider() + StatCell( + label = "Fix", + value = if (fix) "OK" else "Wait", valueColor = if (fix) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentAmber, - textMuted = TrackProTheme.colors.textMuted + size = StatCellSize.Small, + horizontalAlignment = Alignment.CenterHorizontally ) } HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) // ── Telemetry List ──────────────────────── - SectionHeader("DATA STREAM", com.example.trackpro.extrasForUI.TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine) + SectionLabel("Data Stream", modifier = Modifier.padding(horizontal = Spacing.lg, vertical = Spacing.sm)) Column( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(14.dp) + .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) + TelemetryRow("Remote IP", "$ip:$port", TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted) } - 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) - TelemetryRow("ALTITUDE", gpsData?.altitude?.let { String.format("%.1f m", it) } ?: "—", TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted) + 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) + TelemetryRow("Altitude", gpsData?.altitude?.let { String.format("%.1f m", it) } ?: "—", TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted) TelemetryRow( - "REFRESH", + "Refresh", if (useExternal) "20-25 Hz" else "1-5 Hz", if (useExternal) TrackProTheme.colors.accentBlue else TrackProTheme.colors.accentAmber, TrackProTheme.colors.textMuted @@ -198,13 +184,13 @@ fun ESPConnectionTestScreen() { HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) // ── Raw Packet / Debug ──────────────────── - SectionHeader("RAW PACKET", TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine) + SectionLabel("Raw Packet", modifier = Modifier.padding(horizontal = Spacing.lg, vertical = Spacing.sm)) Box( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 16.dp) + .padding(horizontal = Spacing.lg, vertical = Spacing.md) ) { Text( text = gpsData?.toString() ?: "Awaiting data stream...", @@ -214,7 +200,7 @@ fun ESPConnectionTestScreen() { lineHeight = 16.sp ) } - Spacer(Modifier.height(40.dp)) + Spacer(Modifier.height(Spacing.xl)) } } } @@ -245,7 +231,7 @@ fun StyledSpeedometer( // Background arc track drawArc( - color = Color(0xFF1E2530), + color = Color(DataVizColors.gaugeTrack.toColorInt()), startAngle = startAngle, sweepAngle = sweepTotal, useCenter = false, @@ -259,9 +245,9 @@ fun StyledSpeedometer( if (speedFraction > 0f) { // Color shifts from green → amber → red as speed increases val arcColor = when { - speedFraction < 0.5f -> Color(0xFF00C853) - speedFraction < 0.8f -> Color(0xFFFFC107) - else -> Color(0xFFE8001C) + speedFraction < 0.5f -> Color(DataVizColors.gaugeLow.toColorInt()) + speedFraction < 0.8f -> Color(DataVizColors.gaugeMid.toColorInt()) + else -> Color(DataVizColors.gaugeHigh.toColorInt()) } drawArc( color = arcColor, @@ -276,12 +262,12 @@ fun StyledSpeedometer( // Tick marks every 20 km/h val tickPaint = android.graphics.Paint().apply { - color = "#6B7280".toColorInt() + color = DataVizColors.gaugeTick.toColorInt() strokeWidth = 2f isAntiAlias = true } val labelPaint = android.graphics.Paint().apply { - color = "#6B7280".toColorInt() + color = DataVizColors.gaugeTick.toColorInt() textSize = 22f textAlign = android.graphics.Paint.Align.CENTER typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) @@ -320,7 +306,7 @@ fun StyledSpeedometer( // Needle glow (wider, semi-transparent) drawLine( - color = Color(0xFFE8001C).copy(alpha = 0.2f), + color = Color(DataVizColors.gaugeHigh.toColorInt()).copy(alpha = 0.2f), start = Offset(cx, cy), end = Offset( (cx + cos(needleAngle) * needleLength).toFloat(), @@ -331,7 +317,7 @@ fun StyledSpeedometer( ) // Needle sharp drawLine( - color = Color(0xFFE8001C), + color = Color(DataVizColors.gaugeHigh.toColorInt()), start = Offset(cx, cy), end = Offset( (cx + cos(needleAngle) * needleLength).toFloat(), @@ -342,9 +328,9 @@ fun StyledSpeedometer( ) // Center hub - drawCircle(color = Color(0xFF0E1117), radius = 10.dp.toPx(), center = Offset(cx, cy)) + drawCircle(color = Color(DataVizColors.darkOutline.toColorInt()), radius = 10.dp.toPx(), center = Offset(cx, cy)) drawCircle( - color = Color(0xFFE8001C), + color = Color(DataVizColors.gaugeHigh.toColorInt()), radius = 6.dp.toPx(), center = Offset(cx, cy) ) @@ -355,10 +341,8 @@ fun StyledSpeedometer( Spacer(Modifier.height(60.dp)) Text( text = "${animatedSpeed.toInt()}", - color = textPrimary, - fontSize = 52.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-1).sp + style = TrackProType.displayNumeric, + color = textPrimary ) } } @@ -366,20 +350,6 @@ fun StyledSpeedometer( // ── Sub-components ───────────────────────────────────────── -@Composable -private fun SectionHeader(title: String, textMuted: Color, sectorLine: Color) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF0A0C11)) - .padding(horizontal = 24.dp, vertical = 8.dp) - ) { - Text(title, color = textMuted, fontSize = 9.sp, - fontWeight = FontWeight.Black, letterSpacing = 3.sp) - } - HorizontalDivider(color = sectorLine, thickness = 1.dp) -} - @Composable private fun TelemetryRow(label: String, value: String, textPrimary: Color, textMuted: Color) { Row( @@ -387,28 +357,12 @@ private fun TelemetryRow(label: String, value: String, textPrimary: Color, textM horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(label, color = textMuted, fontSize = 10.sp, - letterSpacing = 2.sp, fontWeight = FontWeight.Bold) - Text(value, color = textPrimary, fontSize = 14.sp, - fontWeight = FontWeight.Bold, - fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace) + Text(label.uppercase(), style = TrackProType.label, color = textMuted) + Text( + value, + style = TrackProType.body.copy(fontSize = 13.sp, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace), + color = textPrimary + ) } } -@Composable -private fun SignalCell(label: String, value: String, valueColor: Color, textMuted: Color) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(label, color = textMuted, fontSize = 9.sp, - letterSpacing = 1.sp, fontWeight = FontWeight.Bold) - Text(value, color = valueColor, fontSize = 13.sp, fontWeight = FontWeight.Black) - } -} - -@Composable -private fun VerticalDividerLine(color: Color) { - Box(modifier = Modifier - .width(1.dp) - .height(36.dp) - .background(color)) -} - 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 b8ca13a..b68c345 100644 --- a/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/SettingsScreen.kt @@ -1,23 +1,17 @@ package com.example.trackpro.screens -import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.TextButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -25,11 +19,15 @@ 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.font.FontWeight 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.TrackProTheme +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProType @Composable fun SettingsScreen(onBack: () -> Unit) { @@ -44,193 +42,105 @@ fun SettingsScreen(onBack: () -> Unit) { .fillMaxSize() .background(TrackProTheme.colors.bgDeep) ) { - // --- Header --- - HeaderSection(onBack = onBack, title = "SETTINGS") + AppTopBar(title = "Settings", accent = TrackProTheme.colors.textMuted, onBack = onBack) Column( modifier = Modifier - .padding(16.dp) + .padding(Spacing.md) .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) + verticalArrangement = Arrangement.spacedBy(Spacing.md) ) { // --- Section: Hardware & GPS --- - SettingsSectionHeader(title = "HARDWARE & SENSORS") - - SettingsCard { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = "GPS SOURCE", - color = TrackProTheme.colors.textPrimary, - fontSize = 14.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = if (useExternal) "External ESP32 Module" else "Internal Phone GPS", - color = if (useExternal) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textMuted, - fontSize = 12.sp - ) - } - - // The Toggle Button - Button( - onClick = { app.useExternalGps.value = !useExternal }, - colors = ButtonDefaults.buttonColors( - containerColor = if (useExternal) TrackProTheme.colors.accentCyan else TrackProTheme.colors.bgElevated - ), - shape = RoundedCornerShape(4.dp), - modifier = Modifier.height(36.dp) - ) { - Text( - text = if (useExternal) "USE PHONE" else "USE ESP32", - color = if (useExternal) Color.Black else TrackProTheme.colors.textPrimary, - fontSize = 10.sp, - fontWeight = FontWeight.ExtraBold - ) - } - } + 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 } + ) } // --- Section: Appearance --- - SettingsSectionHeader(title = "APPEARANCE") - SettingsCard { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = "THEME", - color = TrackProTheme.colors.textPrimary, - fontSize = 14.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = if (useDarkTheme) "Dark" else "Light", - color = TrackProTheme.colors.accentCyan, - fontSize = 12.sp - ) - } - - Button( - onClick = { app.setDarkTheme(!useDarkTheme) }, - colors = ButtonDefaults.buttonColors( - containerColor = TrackProTheme.colors.accentCyan - ), - shape = RoundedCornerShape(4.dp), - modifier = Modifier.height(36.dp) - ) { - Text( - text = if (useDarkTheme) "USE LIGHT" else "USE DARK", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.ExtraBold - ) - } - } + SectionLabel("Appearance") + AppCard { + SettingsToggleRow( + label = "Theme", + valueText = if (useDarkTheme) "Dark" else "Light", + valueColor = TrackProTheme.colors.accentCyan, + buttonText = if (useDarkTheme) "Use Light" else "Use Dark", + isActive = true, + onClick = { app.setDarkTheme(!useDarkTheme) } + ) } // --- Section: Units --- - SettingsSectionHeader(title = "UNITS") - SettingsCard { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = "SPEED & DISTANCE", - color = TrackProTheme.colors.textPrimary, - fontSize = 14.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = if (useMetric) "Metric (km/h, km)" else "Imperial (mph, mi)", - color = TrackProTheme.colors.accentCyan, - fontSize = 12.sp - ) - } - - Button( - onClick = { app.setMetricUnits(!useMetric) }, - colors = ButtonDefaults.buttonColors( - containerColor = TrackProTheme.colors.accentCyan - ), - shape = RoundedCornerShape(4.dp), - modifier = Modifier.height(36.dp) - ) { - Text( - text = if (useMetric) "USE MPH" else "USE KM/H", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.ExtraBold - ) - } - } + SectionLabel("Units") + AppCard { + SettingsToggleRow( + label = "Speed & Distance", + valueText = if (useMetric) "Metric (km/h, km)" else "Imperial (mph, mi)", + valueColor = TrackProTheme.colors.accentCyan, + buttonText = if (useMetric) "Use mph" else "Use km/h", + isActive = true, + onClick = { app.setMetricUnits(!useMetric) } + ) } // --- Section: System --- - SettingsSectionHeader(title = "APPLICATION") - SettingsCard { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + SectionLabel("Application") + AppCard { + Column(verticalArrangement = Arrangement.spacedBy(Spacing.sm)) { SettingsInfoRow(label = "App Version", value = "1.0.4-PRO") SettingsInfoRow(label = "Database Status", value = "Connected") SettingsInfoRow(label = "Map & Track Data", value = "© OpenStreetMap contributors") } } + + Spacer(Modifier.height(Spacing.md)) } } } +/** + * 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 + * hand-roll this same Row+Button block. + */ @Composable -private fun SettingsSectionHeader(title: String) { - Text( - text = title, - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - letterSpacing = 2.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 4.dp, bottom = 4.dp) - ) -} - -@Composable -private fun SettingsCard(content: @Composable () -> Unit) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(8.dp)) - .border(1.dp, TrackProTheme.colors.sectorLine, RoundedCornerShape(8.dp)) - .padding(16.dp) +private fun SettingsToggleRow( + label: String, + valueText: String, + valueColor: Color, + buttonText: String, + isActive: Boolean, + onClick: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - content() + Column { + Text(label.uppercase(), style = TrackProType.label, color = TrackProTheme.colors.textPrimary) + Spacer(Modifier.height(2.dp)) + Text(valueText, style = TrackProType.body, color = valueColor) + } + ToggleChip( + text = buttonText, + selected = isActive, + onClick = onClick, + accent = TrackProTheme.colors.accentCyan + ) } } @Composable private fun SettingsInfoRow(label: String, value: String) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text(label, color = TrackProTheme.colors.textMuted, fontSize = 14.sp) - Text(value, color = TrackProTheme.colors.textPrimary, fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(label, style = TrackProType.body, color = TrackProTheme.colors.textMuted) + Text(value, style = TrackProType.body, color = TrackProTheme.colors.textPrimary) } } - -@Composable -private fun HeaderSection(onBack: () -> Unit, title: String) { - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - TextButton(onClick = onBack) { - Text("← BACK", color = TrackProTheme.colors.accentCyan, fontWeight = FontWeight.Bold) - } - Spacer(modifier = Modifier.weight(1f)) - Text(title, color = TrackProTheme.colors.textPrimary, fontWeight = FontWeight.Black, letterSpacing = 2.sp) - } -} \ No newline at end of file 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 8578222..4069e0a 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackBuilderScreen.kt @@ -3,7 +3,6 @@ import android.content.Context import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,10 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -35,7 +31,6 @@ import androidx.compose.runtime.rememberCoroutineScope 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.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -51,6 +46,14 @@ import com.example.trackpro.dataClasses.TrackCoordinatesData import com.example.trackpro.dataClasses.TrackMainData import com.example.trackpro.dataClasses.LatLonOffset import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppCard +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.PrimaryButton +import com.example.trackpro.components.ToggleChip +import com.example.trackpro.theme.DataVizColors +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType import com.example.trackpro.managerClasses.ESPDatabase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -112,13 +115,13 @@ fun TrackBuilderScreen( Box(modifier = Modifier.fillMaxSize().background(TrackProTheme.colors.bgDeep)) { Column(modifier = Modifier.fillMaxSize()) { - HeaderSection(onBack) + AppTopBar(title = "Track Builder", accent = TrackProTheme.colors.accentAmber, onBack = onBack) - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(Spacing.md)) { TrackInfoCard(trackName, countryName, trackMode) { showInfoDialog = true } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(Spacing.md)) ModeToggle(builderType) { builderType = it } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(Spacing.md)) if (builderType == 0) { LiveControls( @@ -169,7 +172,7 @@ fun TrackBuilderScreen( ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(Spacing.sm)) MarkSectorButton( count = sectorCount, enabled = gpsPointsList.isNotEmpty() && (builderType == 1 || isLiveRecording), @@ -178,9 +181,9 @@ fun TrackBuilderScreen( } // Map/Preview Area - Box(modifier = Modifier.weight(1f).fillMaxWidth().padding(16.dp) - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(12.dp)) - .border(1.dp, Color(0xFF1E2530), RoundedCornerShape(12.dp)) + Box(modifier = Modifier.weight(1f).fillMaxWidth().padding(Spacing.md) + .background(TrackProTheme.colors.bgCard, TrackProShapes.card) + .border(1.dp, TrackProTheme.colors.sectorLine, TrackProShapes.card) ) { MapLibreBuilderView(trackMode,points = gpsPointsList, onMapTap = { latLng -> @@ -223,7 +226,7 @@ fun MapLibreBuilderView( map.addPolyline( PolylineOptions() // Corrected reference .addAll(latLngs) - .color("#E8001C".toColorInt()) + .color(DataVizColors.trackLine.toColorInt()) .width(3f) ) } @@ -289,63 +292,53 @@ fun MapLibreBuilderView( @Composable private fun TrackInfoCard(name: String, country: String, mode: String, onClick: () -> Unit) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(8.dp)) - .border(1.dp, Color(0xFF1E2530), RoundedCornerShape(8.dp)) - .padding(16.dp) - ) { - Column { - Text("TRACK CONFIGURATION", color = TrackProTheme.colors.textMuted, fontSize = 10.sp, letterSpacing = 2.sp) - Text(if (name.isEmpty()) "Unnamed Track" else "$name ($country)", color = TrackProTheme.colors.textPrimary, fontSize = 18.sp, fontWeight = FontWeight.Bold) - Text("MODE: ${mode.uppercase()}", color = TrackProTheme.colors.accentCyan, fontSize = 12.sp, fontWeight = FontWeight.Black) - } - Button( - onClick = onClick, - modifier = Modifier.align(Alignment.CenterEnd), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E2530)) - ) { - Text("EDIT", color = TrackProTheme.colors.textPrimary) + AppCard(modifier = Modifier.fillMaxWidth()) { + Box(modifier = Modifier.fillMaxWidth()) { + Column { + Text("Track Configuration", style = TrackProType.label, color = TrackProTheme.colors.textMuted) + Text( + if (name.isEmpty()) "Unnamed Track" else "$name ($country)", + style = TrackProType.titleMedium, + color = TrackProTheme.colors.textPrimary + ) + Text("Mode: ${mode.uppercase()}", style = TrackProType.body.copy(fontSize = 12.sp), color = TrackProTheme.colors.accentCyan) + } + PrimaryButton( + text = "Edit", + onClick = onClick, + accent = TrackProTheme.colors.bgElevated, + contentColor = TrackProTheme.colors.textPrimary, + modifier = Modifier.align(Alignment.CenterEnd) + ) } } } @Composable private fun MarkSectorButton(count: Int, enabled: Boolean, onClick: () -> Unit) { - Button( + PrimaryButton( + text = "Mark Sector ${count + 1}", onClick = onClick, enabled = enabled, - modifier = Modifier.fillMaxWidth().height(48.dp), - colors = ButtonDefaults.buttonColors( - containerColor = TrackProTheme.colors.accentBlue, - disabledContainerColor = Color(0xFF1E2530) - ), - shape = RoundedCornerShape(8.dp) - ) { - Text( - "MARK SECTOR ${count + 1}", - color = if (enabled) Color.Black else TrackProTheme.colors.textMuted, - fontWeight = FontWeight.Bold, - fontSize = 12.sp, - letterSpacing = 1.sp - ) - } + accent = TrackProTheme.colors.accentBlue, + modifier = Modifier.fillMaxWidth().height(48.dp) + ) } @Composable private fun ModeToggle(selected: Int, onSelect: (Int) -> Unit) { - Row(modifier = Modifier.fillMaxWidth().background(TrackProTheme.colors.bgCard, RoundedCornerShape(8.dp)).padding(4.dp)) { - val modes = listOf("LIVE GPS", "MANUAL MAP") + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) + ) { + val modes = listOf("Live GPS", "Manual Map") modes.forEachIndexed { index, label -> - Box( - modifier = Modifier.weight(1f).height(40.dp) - .background(if (selected == index) TrackProTheme.colors.accentCyan else Color.Transparent, RoundedCornerShape(6.dp)) - .padding(4.dp).clickable { onSelect(index) }, - contentAlignment = Alignment.Center - ) { - Text(label, color = if (selected == index) Color.Black else TrackProTheme.colors.textMuted, fontWeight = FontWeight.Bold, fontSize = 12.sp) - } + ToggleChip( + text = label, + selected = selected == index, + onClick = { onSelect(index) }, + modifier = Modifier.weight(1f) + ) } } } @@ -397,62 +390,34 @@ suspend fun endTrackBuilder(context: Context, trackId: Long, isLapTrack: Boolean } -@Composable -fun HeaderSection(onBack: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - TextButton(onClick = onBack) { - Text("← BACK", color = TrackProTheme.colors.accentCyan, fontWeight = FontWeight.Bold) - } - Spacer(modifier = Modifier.weight(1f)) - Text("TRACK BUILDER", color = TrackProTheme.colors.textPrimary, fontWeight = FontWeight.Black, letterSpacing = 2.sp) - } -} - @Composable private fun LiveControls(isRecording: Boolean, onToggle: () -> Unit) { - Button( + PrimaryButton( + text = if (isRecording) "Stop Recording" else "Start GPS Recording", onClick = onToggle, - modifier = Modifier.fillMaxWidth().height(56.dp), - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (isRecording) Color(0xFF330000) else Color(0xFF1E2530) - ), - //border = border(1.dp, if (isRecording) AccentRed else Color.Transparent, RoundedCornerShape(8.dp)) - ) { - val label = if (isRecording) "STOP RECORDING" else "START GPS RECORDING" - val icon = if (isRecording) "■" else "●" - Text("$icon $label", color = if (isRecording) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textPrimary, fontWeight = FontWeight.Bold) - } + accent = if (isRecording) TrackProTheme.colors.danger.copy(alpha = 0.18f) else TrackProTheme.colors.bgElevated, + contentColor = if (isRecording) TrackProTheme.colors.danger else TrackProTheme.colors.textPrimary, + modifier = Modifier.fillMaxWidth().height(56.dp) + ) } @Composable private fun ManualControls(onUndo: () -> Unit, onSave: () -> Unit, canSave: Boolean) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Button( + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Spacing.md)) { + PrimaryButton( + text = "Undo Last", onClick = onUndo, - modifier = Modifier.weight(1f).height(56.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E2530)), - shape = RoundedCornerShape(8.dp) - ) { - Text("UNDO LAST", color = TrackProTheme.colors.textPrimary) - } - Button( + accent = TrackProTheme.colors.bgElevated, + contentColor = TrackProTheme.colors.textPrimary, + modifier = Modifier.weight(1f).height(56.dp) + ) + PrimaryButton( + text = "Save Track", onClick = onSave, enabled = canSave, - modifier = Modifier.weight(1f).height(56.dp), - colors = ButtonDefaults.buttonColors( - containerColor = TrackProTheme.colors.accentCyan, - disabledContainerColor = Color(0xFF2A1014) - ), - shape = RoundedCornerShape(8.dp) - ) { - Text("SAVE TRACK", color = if (canSave) Color.Black else TrackProTheme.colors.textMuted, fontWeight = FontWeight.Bold) - } + accent = TrackProTheme.colors.accentCyan, + modifier = Modifier.weight(1f).height(56.dp) + ) } } diff --git a/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt b/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt index 6cd1a1b..bdc36ba 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackScreen.kt @@ -1,7 +1,6 @@ package com.example.trackpro.screens import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -13,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text @@ -29,9 +27,7 @@ import androidx.compose.runtime.rememberCoroutineScope 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.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView @@ -41,6 +37,13 @@ import com.example.trackpro.TrackProApp import com.example.trackpro.dataClasses.TrackCoordinatesData import com.example.trackpro.dataClasses.TrackMainData import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.SectionLabel +import com.example.trackpro.components.StatCell +import com.example.trackpro.components.ToggleChip +import com.example.trackpro.theme.DataVizColors +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProType import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.managerClasses.timeAttackManagers.TrackGeometry import com.example.trackpro.managerClasses.utilities.UnitFormatter @@ -102,67 +105,47 @@ fun TrackView(database: ESPDatabase, trackId: Long) { ) { Column(modifier = Modifier.fillMaxSize()) { - // ── Top bar ─────────────────────────────────────── - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentBlue) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Text( - text = "● TRACK OVERVIEW", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) - } + AppTopBar(title = "Track Overview", accent = TrackProTheme.colors.accentBlue) // ── Track info card ─────────────────────────────── Column( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(horizontal = Spacing.lg, vertical = Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.xs) ) { Text( text = trackInfo.value.trackName, - color = TrackProTheme.colors.textPrimary, - fontSize = 28.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-0.5).sp + style = TrackProType.titleLarge, + color = TrackProTheme.colors.textPrimary ) Text( text = "${trackInfo.value.country} · ${trackInfo.value.type}", - color = TrackProTheme.colors.textMuted, - fontSize = 12.sp, - letterSpacing = 1.sp + style = TrackProType.body, + color = TrackProTheme.colors.textMuted ) HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(top = Spacing.xs), horizontalArrangement = Arrangement.SpaceBetween ) { - TrackStatCol( - label = "LENGTH", + StatCell( + label = "Length", // totalLength is stored in km; formatDistance takes meters, so convert // first. This also makes the unit dynamic instead of a hardcoded "km". value = trackInfo.value.totalLength?.let { UnitFormatter.formatDistance(it * 1000.0, useMetric) } ?: "?", - textPrimary = TrackProTheme.colors.textPrimary, - textMuted = TrackProTheme.colors.textMuted + horizontalAlignment = Alignment.CenterHorizontally ) - TrackStatCol( - label = "TYPE", + StatCell( + label = "Type", value = trackInfo.value.type.uppercase(), - textPrimary = TrackProTheme.colors.textPrimary, - textMuted = TrackProTheme.colors.textMuted + horizontalAlignment = Alignment.CenterHorizontally ) - TrackStatCol( - label = "CORNERS", + StatCell( + label = "Corners", value = "${trackParts.size}", - textPrimary = TrackProTheme.colors.textPrimary, - textMuted = TrackProTheme.colors.textMuted + horizontalAlignment = Alignment.CenterHorizontally ) } } @@ -212,8 +195,7 @@ fun TrackView(database: ESPDatabase, trackId: Long) { strokeWidth = 2.dp ) Spacer(Modifier.height(12.dp)) - Text("LOADING TRACK DATA", color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, letterSpacing = 2.sp) + Text("Loading track data", style = TrackProType.label, color = TrackProTheme.colors.textFaint) } } } @@ -222,15 +204,6 @@ fun TrackView(database: ESPDatabase, trackId: Long) { } } -@Composable -private fun TrackStatCol(label: String, value: String, textPrimary: Color, textMuted: Color) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(label, color = textMuted, fontSize = 9.sp, - letterSpacing = 2.sp, fontWeight = FontWeight.Bold) - Text(value, color = textPrimary, fontSize = 16.sp, fontWeight = FontWeight.Black) - } -} - /** * Lets the user auto-slice an already-saved track into N even sectors after the fact, * instead of only being able to mark them by hand while live-recording. Picking a count @@ -249,68 +222,42 @@ private fun SectorSlicerCard( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + .padding(horizontal = Spacing.lg, vertical = Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { + SectionLabel("Sectors") Text( - text = "SECTORS", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - letterSpacing = 2.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = if (sectorCount > 0) "$sectorCount SECTORS MARKED" else "NONE MARKED", - color = if (sectorCount > 0) TrackProTheme.colors.accentBlue else TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Black + 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 ) } Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { (2..6).forEach { n -> - val active = sectorCount == n - Box( - modifier = Modifier - .weight(1f) - .background( - if (active) TrackProTheme.colors.accentBlue else TrackProTheme.colors.bgElevated, - RoundedCornerShape(6.dp) - ) - .border( - 1.dp, - if (active) Color.Transparent else TrackProTheme.colors.sectorLine, - RoundedCornerShape(6.dp) - ) - .clickable { onSlice(n) } - .padding(vertical = 10.dp), - contentAlignment = Alignment.Center - ) { - Text( - "$n", - color = if (active) Color.Black else TrackProTheme.colors.textPrimary, - fontWeight = FontWeight.Black, - fontSize = 14.sp - ) - } + ToggleChip( + text = "$n", + selected = sectorCount == n, + onClick = { onSlice(n) }, + accent = TrackProTheme.colors.accentBlue, + modifier = Modifier.weight(1f) + ) } } if (sectorCount > 0) { Text( - text = "CLEAR SECTORS", - color = TrackProTheme.colors.deltaBad, - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp, + text = "Clear sectors", + style = TrackProType.label, + color = TrackProTheme.colors.danger, modifier = Modifier.clickable { onClear() } ) } @@ -374,7 +321,7 @@ fun TrackStaticMapView( style.addLayer( LineLayer("track-static-layer", "track-static-src").apply { setProperties( - PropertyFactory.lineColor("#00C853"), + PropertyFactory.lineColor(DataVizColors.trackLine), PropertyFactory.lineWidth(3f), PropertyFactory.lineCap(Property.LINE_CAP_ROUND) ) @@ -389,9 +336,9 @@ fun TrackStaticMapView( style.addLayer( CircleLayer("start-layer", "start-src").apply { setProperties( - PropertyFactory.circleColor("#E8001C"), + PropertyFactory.circleColor(DataVizColors.startMarker), PropertyFactory.circleRadius(8f), - PropertyFactory.circleStrokeColor("#FFFFFF"), + PropertyFactory.circleStrokeColor(DataVizColors.boundaryLine), PropertyFactory.circleStrokeWidth(2f) ) } @@ -407,9 +354,9 @@ fun TrackStaticMapView( style.addLayer( CircleLayer("sector-layer", "sector-src").apply { setProperties( - PropertyFactory.circleColor("#FBBF24"), + PropertyFactory.circleColor(DataVizColors.sectorMarker), PropertyFactory.circleRadius(6f), - PropertyFactory.circleStrokeColor("#FFFFFF"), + PropertyFactory.circleStrokeColor(DataVizColors.boundaryLine), PropertyFactory.circleStrokeWidth(1.5f) ) } 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 cca697c..9b83fff 100644 --- a/app/src/main/java/com/example/trackpro/screens/TrackVehicleSelectorScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/TrackVehicleSelectorScreen.kt @@ -4,7 +4,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -12,9 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -26,8 +22,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.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel @@ -37,6 +31,11 @@ import com.example.trackpro.TrackProApp import com.example.trackpro.extrasForUI.DropdownMenuFieldMulti import com.example.trackpro.extrasForUI.TrackDropdownMenu import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppCard +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.PrimaryButton +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProType import com.example.trackpro.viewModels.TrackViewModel import com.example.trackpro.viewModels.TrackViewModelFactory import com.example.trackpro.viewModels.VehicleViewModel @@ -63,80 +62,70 @@ fun TrackVehicleSelectorScreen( } Box(modifier = Modifier.fillMaxSize().background(TrackProTheme.colors.bgDeep)) { - Column( - modifier = Modifier.fillMaxSize().padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // ── Header ─────────────────────────────────────── - Text( - "SESSION SETUP", - color = TrackProTheme.colors.textPrimary, - fontSize = 24.sp, - fontWeight = FontWeight.Black, - letterSpacing = 2.sp, - modifier = Modifier.padding(bottom = 32.dp, top = 16.dp) + Column(modifier = Modifier.fillMaxSize()) { + AppTopBar( + title = "Session Setup", + accent = TrackProTheme.colors.accentCyan, + onBack = { navController.popBackStack() } ) - // ── Track Selection Card ───────────────────────── - SelectionCard( - label = "CIRCUIT", - title = selectedTrackName.ifEmpty { "Select Track" }, - isSet = selectedTrackId != -1L + Column( + modifier = Modifier.fillMaxSize().padding(Spacing.lg), + horizontalAlignment = Alignment.CenterHorizontally ) { - TrackDropdownMenu( - label = "Choose Location", - tracks = tracks, - selectedTrackName = selectedTrackName, - onTrackSelected = { id -> - val track = tracks.find { it.trackId == id } - selectedTrackName = track?.trackName ?: "" - selectedTrackId = id - } - ) - } + Spacer(modifier = Modifier.height(Spacing.md)) + + // ── Track Selection Card ───────────────────────── + SelectionCard( + label = "Circuit", + title = selectedTrackName.ifEmpty { "Select Track" }, + isSet = selectedTrackId != -1L + ) { + TrackDropdownMenu( + label = "Choose Location", + tracks = tracks, + selectedTrackName = selectedTrackName, + onTrackSelected = { id -> + val track = tracks.find { it.trackId == id } + selectedTrackName = track?.trackName ?: "" + selectedTrackId = id + } + ) + } - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(Spacing.md)) - // ── Vehicle Selection Card ─────────────────────── - SelectionCard( - label = "VEHICLE", - title = selectedVehicleName.ifEmpty { "Select Vehicle" }, - isSet = selectedVehicleId != -1L - ) { - if (vehicles.isNotEmpty()) { - DropdownMenuFieldMulti( - "Choose Machine", - vehicles, - selectedVehicleName - ) { id -> - selectedVehicleId = id - selectedVehicleName = vehicles.find { it.vehicleId == id }?.manufacturerAndModel ?: "" // Adjust 'name' to your vehicle field + // ── Vehicle Selection Card ─────────────────────── + SelectionCard( + label = "Vehicle", + title = selectedVehicleName.ifEmpty { "Select Vehicle" }, + isSet = selectedVehicleId != -1L + ) { + if (vehicles.isNotEmpty()) { + DropdownMenuFieldMulti( + "Choose Machine", + vehicles, + selectedVehicleName + ) { id -> + selectedVehicleId = id + 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) } - } else { - Text("No vehicles found in garage", color = TrackProTheme.colors.accentCyan, fontSize = 12.sp) } - } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - // ── Start Action ───────────────────────────────── - val canStart = selectedVehicleId != -1L && selectedTrackId != -1L + // ── Start Action ───────────────────────────────── + val canStart = selectedVehicleId != -1L && selectedTrackId != -1L - Button( - onClick = { navController.navigate("timeattack/$selectedVehicleId/$selectedTrackId") }, - enabled = canStart, - modifier = Modifier.fillMaxWidth().height(64.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (canStart) TrackProTheme.colors.accentCyan else Color(0xFF2A1014), - contentColor = if (canStart) Color.White else TrackProTheme.colors.textMuted - ), - shape = RoundedCornerShape(8.dp) - ) { - Text( - "START TIME ATTACK", - fontWeight = FontWeight.Black, - letterSpacing = 1.sp, - fontSize = 18.sp + PrimaryButton( + text = "Start Time Attack", + onClick = { navController.navigate("timeattack/$selectedVehicleId/$selectedTrackId") }, + enabled = canStart, + accent = TrackProTheme.colors.accentCyan, + modifier = Modifier.fillMaxWidth().height(56.dp) ) } } @@ -150,16 +139,17 @@ fun SelectionCard( isSet: Boolean, content: @Composable () -> Unit ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(12.dp)) - .border(1.dp, if (isSet) TrackProTheme.colors.accentCyan.copy(alpha = 0.5f) else Color(0xFF1E2530), RoundedCornerShape(12.dp)) - .padding(16.dp) + AppCard( + modifier = Modifier.fillMaxWidth(), + borderColor = if (isSet) TrackProTheme.colors.accentCyan.copy(alpha = 0.5f) else TrackProTheme.colors.sectorLine ) { - Text(label, color = if (isSet) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textMuted, fontSize = 10.sp, fontWeight = FontWeight.Bold) - Text(title, color = TrackProTheme.colors.textPrimary, fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(vertical = 4.dp)) - Spacer(modifier = Modifier.height(8.dp)) + Text( + label.uppercase(), + style = TrackProType.label, + color = if (isSet) TrackProTheme.colors.accentCyan 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)) content() } } 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 534090c..56ef5e7 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 @@ -9,13 +9,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -37,13 +34,19 @@ 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.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.example.trackpro.dataClasses.VehicleInformationData import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.EmptyState +import com.example.trackpro.components.StatCell +import com.example.trackpro.components.StatCellDivider +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.viewModels.VehicleFULLViewModel import kotlinx.coroutines.Dispatchers @@ -64,58 +67,28 @@ fun CarListScreen(navController: NavController, viewModel: VehicleFULLViewModel) ) { Column(modifier = Modifier.fillMaxSize()) { - // ── Top bar ─────────────────────────────────── - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentAmber) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { + AppTopBar( + title = "My Vehicles", + accent = TrackProTheme.colors.accentAmber, + trailing = { Text( - text = "● MY VEHICLES", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) - Text( - text = "${vehicles.size} CARS", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - letterSpacing = 2.sp + text = "${vehicles.size} cars", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) } - } + ) if (vehicles.isEmpty()) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = "NO VEHICLES YET", - color = TrackProTheme.colors.textMuted, - fontSize = 14.sp, - letterSpacing = 3.sp, - fontWeight = FontWeight.Black - ) - Spacer(Modifier.height(8.dp)) - Text( - text = "Add a vehicle from the main screen", - color = TrackProTheme.colors.textMuted.copy(alpha = 0.5f), - fontSize = 12.sp - ) - } - } + EmptyState( + message = "No vehicles yet", + hint = "Add a vehicle from the main screen" + ) } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + contentPadding = PaddingValues(Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { items(vehicles) { vehicle -> VehicleCard( @@ -124,7 +97,7 @@ fun CarListScreen(navController: NavController, viewModel: VehicleFULLViewModel) bgCard = TrackProTheme.colors.bgCard, bgElevated = TrackProTheme.colors.bgElevated, accentAmber = TrackProTheme.colors.accentAmber, - accentRed = TrackProTheme.colors.accentCyan, + dangerColor = TrackProTheme.colors.danger, textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted, sectorLine = TrackProTheme.colors.sectorLine, @@ -149,7 +122,7 @@ fun VehicleCard( bgCard: Color, bgElevated: Color, accentAmber: Color, - accentRed: Color, + dangerColor: Color, textPrimary: Color, textMuted: Color, sectorLine: Color, @@ -168,15 +141,15 @@ fun VehicleCard( onDelete(vehicle) showDeleteDialog = false }) { - Text("DELETE", color = accentRed, fontWeight = FontWeight.Black) + Text("Delete", color = dangerColor, style = TrackProType.titleMedium) } }, dismissButton = { TextButton(onClick = { showDeleteDialog = false }) { - Text("CANCEL", color = textMuted) + Text("Cancel", color = textMuted) } }, - title = { Text("Delete Vehicle?", fontWeight = FontWeight.Black) }, + title = { Text("Delete Vehicle?") }, text = { Text("${vehicle.manufacturer} ${vehicle.model} will be permanently removed.") } ) } @@ -184,8 +157,8 @@ fun VehicleCard( Box( modifier = Modifier .fillMaxWidth() - .background(bgCard, RoundedCornerShape(12.dp)) - .border(1.dp, sectorLine, RoundedCornerShape(12.dp)) + .background(bgCard, TrackProShapes.card) + .border(1.dp, sectorLine, TrackProShapes.card) .clickable { navController.navigate("vehicle/${vehicle.vehicleId}") } ) { Column { @@ -196,9 +169,9 @@ fun VehicleCard( .fillMaxWidth() .background( accentAmber.copy(alpha = 0.12f), - RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) + RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) ) - .padding(horizontal = 16.dp, vertical = 10.dp) + .padding(horizontal = Spacing.md, vertical = Spacing.sm) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -207,45 +180,38 @@ fun VehicleCard( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { Box( modifier = Modifier - .background( - accentAmber.copy(alpha = 0.15f), - RoundedCornerShape(3.dp) - ) + .background(accentAmber.copy(alpha = 0.15f), TrackProShapes.badge) .padding(horizontal = 6.dp, vertical = 2.dp) ) { Text( text = vehicle.fuelType.uppercase(), - color = accentAmber, - fontSize = 8.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + style = TrackProType.label.copy(fontSize = 9.sp), + color = accentAmber ) } Text( text = vehicle.drivetrain.uppercase(), - color = textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp + style = TrackProType.label.copy(fontSize = 9.sp), + color = textMuted ) } // Delete button Box( modifier = Modifier - .size(28.dp) - .background(accentRed.copy(alpha = 0.1f), RoundedCornerShape(4.dp)) + .size(26.dp) + .background(dangerColor.copy(alpha = 0.1f), TrackProShapes.badge) .clickable { showDeleteDialog = true }, contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Close, contentDescription = "Delete", - tint = accentRed, + tint = dangerColor, modifier = Modifier.size(14.dp) ) } @@ -254,23 +220,20 @@ fun VehicleCard( // ── Vehicle name ──────────────────────────── Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + modifier = Modifier.padding(horizontal = Spacing.md, vertical = Spacing.sm), verticalArrangement = Arrangement.spacedBy(2.dp) ) { Text( text = "${vehicle.manufacturer} ${vehicle.model}", + style = TrackProType.titleMedium, color = textPrimary, - fontSize = 20.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-0.3).sp, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( text = "${vehicle.year} · ${vehicle.engineType}", - color = textMuted, - fontSize = 12.sp, - fontWeight = FontWeight.Bold + style = TrackProType.body.copy(fontSize = 12.sp), + color = textMuted ) } @@ -282,88 +245,27 @@ fun VehicleCard( .fillMaxWidth() .background( bgElevated, - RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp) + RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp) ) - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(horizontal = Spacing.md, vertical = Spacing.sm), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - VehicleStatMini( - label = "POWER", - value = "${vehicle.horsepower}", - unit = "hp", - accentAmber = accentAmber, - textMuted = textMuted - ) - Box( - modifier = Modifier - .width(1.dp) - .height(32.dp) - .background(sectorLine) - ) - VehicleStatMini( - label = "TORQUE", - value = vehicle.torque?.toString() ?: "—", - unit = "Nm", - accentAmber = accentAmber, - textMuted = textMuted - ) - Box( - modifier = Modifier - .width(1.dp) - .height(32.dp) - .background(sectorLine) - ) - VehicleStatMini( - label = "WEIGHT", - value = "${vehicle.weight}", - unit = "kg", - accentAmber = accentAmber, - textMuted = textMuted - ) + StatCell(label = "Power", value = "${vehicle.horsepower}", unit = "hp", valueColor = accentAmber, horizontalAlignment = Alignment.CenterHorizontally) + StatCellDivider() + StatCell(label = "Torque", value = vehicle.torque?.toString() ?: "—", unit = "Nm", valueColor = accentAmber, horizontalAlignment = Alignment.CenterHorizontally) + StatCellDivider() + StatCell(label = "Weight", value = "${vehicle.weight}", unit = "kg", valueColor = accentAmber, horizontalAlignment = Alignment.CenterHorizontally) Text( - text = "VIEW →", - color = accentAmber.copy(alpha = 0.7f), - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + text = "View", + style = TrackProType.label, + color = accentAmber.copy(alpha = 0.8f) ) } } } } -@Composable -private fun VehicleStatMini( - label: String, - value: String, - unit: String, - accentAmber: Color, - textMuted: Color -) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = label, - color = textMuted, - fontSize = 8.sp, - letterSpacing = 1.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = value, - color = accentAmber, - fontSize = 18.sp, - fontWeight = FontWeight.Black - ) - Text( - text = unit, - color = textMuted, - fontSize = 8.sp, - fontWeight = FontWeight.Bold - ) - } -} - suspend fun DeleteVehicle(context: Context, database: ESPDatabase, vehicleId: Long) { database.vehicleInformationDAO().deleteVehicle(vehicleId) 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 d47efbf..8f0e8dd 100644 --- a/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt +++ b/app/src/main/java/com/example/trackpro/screens/listViewScreens/DragTimesList.kt @@ -2,7 +2,6 @@ package com.example.trackpro.screens.listViewScreens import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -12,7 +11,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -29,12 +27,16 @@ import androidx.compose.runtime.rememberCoroutineScope 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.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.EmptyState +import com.example.trackpro.components.ExpandableGroup +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType import com.example.trackpro.managerClasses.utilities.DateFormatterUtil import com.example.trackpro.models.DragSessionWithVehicle import com.example.trackpro.viewModels.DragSessionViewModel @@ -62,55 +64,25 @@ fun DragTimesListView( .background(TrackProTheme.colors.bgDeep) ) { Column(modifier = Modifier.fillMaxSize()) { - Box( - modifier = Modifier.fillMaxWidth().background(TrackProTheme.colors.accentCyan) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - "● Drag RECORDS", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) + AppTopBar( + title = "Drag Records", + accent = TrackProTheme.colors.accentCyan, + trailing = { Text( - "${dragSessions.size} SESSIONS", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.Bold + "${dragSessions.size} sessions", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) } - } - + ) if (dragSessions.isEmpty()) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - "NO SESSIONS RECORDED", - color = TrackProTheme.colors.textMuted, - fontSize = 14.sp, - letterSpacing = 3.sp, - fontWeight = FontWeight.Black - ) - Spacer(Modifier.height(8.dp)) - Text( - "Run a drag session to see it here", - color = TrackProTheme.colors.textMuted.copy(alpha = 0.5f), - fontSize = 12.sp - ) - } - } + EmptyState(message = "No sessions recorded", hint = "Run a drag session to see it here") } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + contentPadding = PaddingValues(Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { groupedSessions.forEach { (groupKey, sessions) -> item(key = groupKey) { @@ -132,92 +104,49 @@ fun ExpandableSessionGroup( sessions: List, navController: NavController ) { - var isExpanded by remember { mutableStateOf(false) } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(12.dp)) - .border( - 1.dp, - if (isExpanded) TrackProTheme.colors.accentCyan.copy(alpha = 0.5f) else TrackProTheme.colors.sectorLine, - RoundedCornerShape(12.dp) - ) - ) { - // --- THE HEADER (Always Visible) --- - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { isExpanded = !isExpanded } - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { + ExpandableGroup( + accent = TrackProTheme.colors.accentCyan, + header = { Column(modifier = Modifier.weight(1f)) { + Text(groupTitle, style = TrackProType.titleMedium.copy(fontSize = 13.sp), color = TrackProTheme.colors.textPrimary) Text( - groupTitle.uppercase(), - color = if (isExpanded) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textPrimary, - fontSize = 12.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp - ) - Text( - "${sessions.size} RUNS COMPLETED", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Bold + "${sessions.size} runs completed", + style = TrackProType.body.copy(fontSize = 11.sp), + color = TrackProTheme.colors.textMuted ) } - - Text( - if (isExpanded) "CLOSE —" else "VIEW ALL +", - color = if (isExpanded) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Black - ) } + ) { + Column(verticalArrangement = Arrangement.spacedBy(Spacing.xs)) { + sessions.forEach { session -> + val time = DateFormatterUtil.getTimeFormat().format(Date(session.startTime)) - // --- THE CONTENT (Visible when Expanded) --- - if (isExpanded) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp) - .padding(bottom = 12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - sessions.forEach { session -> - val time = DateFormatterUtil.getTimeFormat().format(Date(session.startTime)) - - Row( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.bgElevated, RoundedCornerShape(6.dp)) - .clickable { navController.navigate("graph/${session.sessionId}") } - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - Modifier.size(6.dp).background(TrackProTheme.colors.accentCyan, RoundedCornerShape(100)) - ) - Spacer(Modifier.width(12.dp)) - Text( - "RUN AT $time", - color = TrackProTheme.colors.textPrimary, - fontSize = 13.sp, - fontWeight = FontWeight.Bold - ) - } - + Row( + modifier = Modifier + .fillMaxWidth() + .background(TrackProTheme.colors.bgElevated, TrackProShapes.control) + .clickable { navController.navigate("graph/${session.sessionId}") } + .padding(Spacing.sm), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + Modifier.size(6.dp).background(TrackProTheme.colors.accentCyan, RoundedCornerShape(100)) + ) + Spacer(Modifier.width(Spacing.sm)) Text( - "DETAILS →", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Black + "Run at $time", + style = TrackProType.body, + color = TrackProTheme.colors.textPrimary ) } + + Text( + "Details", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted + ) } } } 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 309b607..c8d5ef8 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 @@ -25,14 +25,17 @@ 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.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.example.trackpro.dataClasses.SessionData import com.example.trackpro.dataClasses.VehicleInformationData import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.EmptyState +import com.example.trackpro.components.ExpandableGroup +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProType import com.example.trackpro.viewModels.SessionViewModel import com.example.trackpro.viewModels.TrackViewModel import com.example.trackpro.viewModels.VehicleFULLViewModel @@ -61,43 +64,21 @@ fun TimeAttackListViewScreen( Box(modifier = Modifier.fillMaxSize().background(TrackProTheme.colors.bgDeep)) { Column(modifier = Modifier.fillMaxSize()) { - // Header Bar - Box( - modifier = Modifier.fillMaxWidth().background(TrackProTheme.colors.accentCyan).padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text("● TRACK RECORDS", color = Color.Black, fontSize = 11.sp, fontWeight = FontWeight.Black, letterSpacing = 3.sp) - Text("${trackSessions.size} SESSIONS", color = Color.Black, fontSize = 10.sp, fontWeight = FontWeight.Bold) + AppTopBar( + title = "Track Records", + accent = TrackProTheme.colors.accentCyan, + trailing = { + Text("${trackSessions.size} sessions", style = TrackProType.label, color = TrackProTheme.colors.textMuted) } - } + ) if (trackSessions.isEmpty()) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - "NO SESSIONS RECORDED", - color = TrackProTheme.colors.textMuted, - fontSize = 14.sp, - letterSpacing = 3.sp, - fontWeight = FontWeight.Black - ) - Spacer(Modifier.height(8.dp)) - Text( - "Run a track session to see it here", - color = TrackProTheme.colors.textMuted.copy(alpha = 0.5f), - fontSize = 12.sp - ) - } - } + EmptyState(message = "No sessions recorded", hint = "Run a track session to see it here") } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + contentPadding = PaddingValues(Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { groupedByTrack.forEach { (trackName, sessions) -> item(key = trackName) { @@ -125,94 +106,53 @@ fun ExpandableTrackGroup( vehicles: List, navController: NavController ) { - var isExpanded by remember { mutableStateOf(false) } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(12.dp)) - .border( - 1.dp, - if (isExpanded) TrackProTheme.colors.accentCyan.copy(alpha = 0.4f) else TrackProTheme.colors.sectorLine, - RoundedCornerShape(12.dp) - ) - ) { - // --- HEADER --- - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { isExpanded = !isExpanded } - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { + ExpandableGroup( + accent = TrackProTheme.colors.accentCyan, + header = { Column(modifier = Modifier.weight(1f)) { + Text(trackName, style = TrackProType.titleMedium.copy(fontSize = 14.sp), color = TrackProTheme.colors.textPrimary) Text( - trackName.uppercase(), - color = if (isExpanded) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textPrimary, - fontSize = 14.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp - ) - Text( - "$trackMeta · ${sessions.size} SESSIONS", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Bold + "$trackMeta · ${sessions.size} sessions", + style = TrackProType.body.copy(fontSize = 11.sp), + color = TrackProTheme.colors.textMuted ) } - Text( - if (isExpanded) "CLOSE —" else "VIEW ALL +", - color = if (isExpanded) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Black - ) } + ) { + Column(verticalArrangement = Arrangement.spacedBy(Spacing.sm)) { + sessions.forEach { session -> + val vehicle = vehicles.find { it.vehicleId == session.vehicleId } + val date = Instant.ofEpochMilli(session.startTime) + .atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("dd MMM")) - // --- EXPANDED SESSIONS --- - if (isExpanded) { - Column( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - sessions.forEach { session -> - val vehicle = vehicles.find { it.vehicleId == session.vehicleId } - val date = Instant.ofEpochMilli(session.startTime) - .atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("dd MMM")) - - Row( - modifier = Modifier - .fillMaxWidth() - .border(1.dp, TrackProTheme.colors.sectorLine.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) - .background(TrackProTheme.colors.bgElevated.copy(alpha = 0.3f), RoundedCornerShape(8.dp)) - .clickable { navController.navigate("timeattacklistitem/${session.id}") } - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - // Vertical "Pillar" accent - Box(modifier = Modifier.width(3.dp).height(24.dp).background(TrackProTheme.colors.accentCyan, RoundedCornerShape(2.dp))) - Spacer(Modifier.width(12.dp)) - Column { - Text( - "${vehicle?.manufacturer} ${vehicle?.model}", - color = TrackProTheme.colors.textPrimary, - fontSize = 13.sp, - fontWeight = FontWeight.Bold - ) - Text( - "SESSION DATE: $date", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp - ) - } - } - - Column(horizontalAlignment = Alignment.End) { - Text("TELEMETRY", color = TrackProTheme.colors.accentCyan, fontSize = 9.sp, fontWeight = FontWeight.Black) - Text("→", color = TrackProTheme.colors.accentCyan, fontSize = 12.sp) + Row( + modifier = Modifier + .fillMaxWidth() + .background(TrackProTheme.colors.bgElevated.copy(alpha = 0.4f), RoundedCornerShape(8.dp)) + .clickable { navController.navigate("timeattacklistitem/${session.id}") } + .padding(Spacing.sm), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + // Vertical "Pillar" accent + Box(modifier = Modifier.width(2.dp).height(24.dp).background(TrackProTheme.colors.accentCyan, RoundedCornerShape(2.dp))) + Spacer(Modifier.width(Spacing.sm)) + Column { + Text( + "${vehicle?.manufacturer} ${vehicle?.model}", + style = TrackProType.body, + color = TrackProTheme.colors.textPrimary + ) + Text( + "Session date: $date", + style = TrackProType.body.copy(fontSize = 10.sp), + color = TrackProTheme.colors.textMuted + ) } } + + Text("Telemetry", style = TrackProType.label, color = TrackProTheme.colors.accentCyan) } } } 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 05b8d3e..8293131 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 @@ -8,10 +8,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -36,7 +34,6 @@ 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.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -44,6 +41,12 @@ import androidx.navigation.NavController import com.example.trackpro.TrackProApp import com.example.trackpro.dataClasses.TrackMainData import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.EmptyState +import com.example.trackpro.components.StatCell +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.managerClasses.utilities.UnitFormatter import com.example.trackpro.viewModels.TrackViewModel @@ -67,51 +70,25 @@ fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) { ) { Column(modifier = Modifier.fillMaxSize()) { - // ── Top bar ─────────────────────────────────── - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentAmber) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "● MY TRACKS", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) + AppTopBar( + title = "My Tracks", + accent = TrackProTheme.colors.accentAmber, + trailing = { Text( - text = "${tracks.size} TRACKS", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - letterSpacing = 2.sp + text = "${tracks.size} tracks", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) } - } + ) if (tracks.isEmpty()) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("NO TRACKS YET", color = TrackProTheme.colors.textMuted, - fontSize = 14.sp, letterSpacing = 3.sp, - fontWeight = FontWeight.Black) - Spacer(Modifier.height(8.dp)) - Text("Build a track to see it here", - color = TrackProTheme.colors.textMuted.copy(alpha = 0.5f), fontSize = 12.sp) - } - } + EmptyState(message = "No tracks yet", hint = "Build a track to see it here") } else { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + contentPadding = PaddingValues(Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { items(tracks) { track -> TrackCard( @@ -122,7 +99,7 @@ fun TrackListScreen(navController: NavController, viewModel: TrackViewModel) { bgCard = TrackProTheme.colors.bgCard, bgElevated = TrackProTheme.colors.bgElevated, accentAmber = TrackProTheme.colors.accentAmber, - accentRed = TrackProTheme.colors.accentCyan, + dangerColor = TrackProTheme.colors.danger, textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted, sectorLine = TrackProTheme.colors.sectorLine, @@ -148,7 +125,7 @@ fun TrackCard( bgCard: Color, bgElevated: Color, accentAmber: Color, - accentRed: Color, + dangerColor: Color, textPrimary: Color, textMuted: Color, sectorLine: Color, @@ -171,15 +148,15 @@ fun TrackCard( textContentColor = TrackProTheme.colors.textMuted, confirmButton = { TextButton(onClick = { onDelete(track); showDeleteDialog = false }) { - Text("DELETE", color = accentRed, fontWeight = FontWeight.Black) + Text("Delete", color = dangerColor, style = TrackProType.titleMedium) } }, dismissButton = { TextButton(onClick = { showDeleteDialog = false }) { - Text("CANCEL", color = textMuted) + Text("Cancel", color = textMuted) } }, - title = { Text("Delete Track?", fontWeight = FontWeight.Black) }, + title = { Text("Delete Track?") }, text = { Text("${track.trackName} will be permanently removed.") } ) } @@ -187,8 +164,8 @@ fun TrackCard( Box( modifier = Modifier .fillMaxWidth() - .background(bgCard, RoundedCornerShape(12.dp)) - .border(1.dp, sectorLine, RoundedCornerShape(12.dp)) + .background(bgCard, TrackProShapes.card) + .border(1.dp, sectorLine, TrackProShapes.card) .clickable { navController.navigate("track/${track.trackId}") } ) { Column { @@ -198,9 +175,9 @@ fun TrackCard( .fillMaxWidth() .background( accentAmber.copy(alpha = 0.12f), - RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) + RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) ) - .padding(horizontal = 16.dp, vertical = 10.dp) + .padding(horizontal = Spacing.md, vertical = Spacing.sm) ) { Row( modifier = Modifier.fillMaxWidth(), @@ -209,43 +186,38 @@ fun TrackCard( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { Box( modifier = Modifier - .background(accentAmber.copy(alpha = 0.15f), - RoundedCornerShape(3.dp)) + .background(accentAmber.copy(alpha = 0.15f), TrackProShapes.badge) .padding(horizontal = 6.dp, vertical = 2.dp) ) { Text( text = track.type.uppercase(), - color = accentAmber, - fontSize = 8.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + style = TrackProType.label.copy(fontSize = 9.sp), + color = accentAmber ) } Text( text = track.country.uppercase(), - color = textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp + style = TrackProType.label.copy(fontSize = 9.sp), + color = textMuted ) } // Delete button Box( modifier = Modifier - .size(28.dp) - .background(accentRed.copy(alpha = 0.1f), RoundedCornerShape(4.dp)) + .size(26.dp) + .background(dangerColor.copy(alpha = 0.1f), TrackProShapes.badge) .clickable { showDeleteDialog = true }, contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Close, contentDescription = "Delete", - tint = accentRed, + tint = dangerColor, modifier = Modifier.size(14.dp) ) } @@ -254,15 +226,13 @@ fun TrackCard( // ── Track name ──────────────────────────────── Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + modifier = Modifier.padding(horizontal = Spacing.md, vertical = Spacing.sm), verticalArrangement = Arrangement.spacedBy(2.dp) ) { Text( text = track.trackName, + style = TrackProType.titleMedium, color = textPrimary, - fontSize = 20.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-0.3).sp, maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -274,39 +244,26 @@ fun TrackCard( Row( modifier = Modifier .fillMaxWidth() - .background(bgElevated, - RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) - .padding(horizontal = 16.dp, vertical = 12.dp), + .background(bgElevated, RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) + .padding(horizontal = Spacing.md, vertical = Spacing.sm), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Column { - Text("LENGTH", color = textMuted, fontSize = 9.sp, - letterSpacing = 1.sp, fontWeight = FontWeight.Bold) - Text( - // totalLength is stored in km; formatDistance takes meters. - text = track.totalLength?.let { UnitFormatter.formatDistance(it * 1000.0, useMetric) } ?: "?", - color = accentAmber, - fontSize = 16.sp, - fontWeight = FontWeight.Black - ) - } - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("LAP RECORD", color = textMuted, fontSize = 9.sp, - letterSpacing = 1.sp, fontWeight = FontWeight.Bold) - Text( - text = bestLapTime ?: "—", - color = textPrimary, - fontSize = 16.sp, - fontWeight = FontWeight.Black - ) - } + StatCell( + label = "Length", + // totalLength is stored in km; formatDistance takes meters. + value = track.totalLength?.let { UnitFormatter.formatDistance(it * 1000.0, useMetric) } ?: "?", + valueColor = accentAmber + ) + StatCell( + label = "Lap Record", + value = bestLapTime ?: "—", + horizontalAlignment = Alignment.CenterHorizontally + ) Text( - text = "VIEW →", - color = accentAmber.copy(alpha = 0.7f), - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + text = "View", + style = TrackProType.label, + color = accentAmber.copy(alpha = 0.8f) ) } } 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 1b82f6d..281a5df 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 @@ -26,13 +26,17 @@ 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.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.example.trackpro.TrackProApp import com.example.trackpro.dataClasses.VehicleInformationData import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.SectionLabel +import com.example.trackpro.components.StatCell +import com.example.trackpro.components.StatCellSize +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProType import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.managerClasses.utilities.UnitFormatter import kotlinx.coroutines.Dispatchers @@ -70,29 +74,14 @@ fun CarViewScreen(vehicleId: Long) { strokeWidth = 2.dp ) Spacer(Modifier.height(12.dp)) - Text("LOADING VEHICLE", color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, letterSpacing = 3.sp) + Text("Loading vehicle", style = TrackProType.label, color = TrackProTheme.colors.textFaint) } } } else { val vehicle = vehicleInfo!! Column(modifier = Modifier.fillMaxSize()) { - // ── Top bar ─────────────────────────────── - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentAmber) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Text( - text = "● VEHICLE PROFILE", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) - } + AppTopBar(title = "Vehicle Profile", accent = TrackProTheme.colors.accentAmber) LazyColumn(modifier = Modifier.fillMaxSize()) { @@ -102,21 +91,17 @@ fun CarViewScreen(vehicleId: Long) { modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 20.dp) + .padding(horizontal = Spacing.lg, vertical = Spacing.md) ) { Text( text = "${vehicle.manufacturer} ${vehicle.model}", - color = TrackProTheme.colors.textPrimary, - fontSize = 28.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-0.5).sp + style = TrackProType.titleLarge, + color = TrackProTheme.colors.textPrimary ) Text( text = "${vehicle.year}", - color = TrackProTheme.colors.textMuted, - fontSize = 14.sp, - letterSpacing = 1.sp, - fontWeight = FontWeight.Bold + style = TrackProType.body, + color = TrackProTheme.colors.textMuted ) } HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) @@ -124,35 +109,17 @@ fun CarViewScreen(vehicleId: Long) { // ── Performance stats ───────────────── item { - VehicleSectionHeader("PERFORMANCE", TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine) + SectionLabel("Performance", modifier = Modifier.padding(horizontal = Spacing.lg, vertical = Spacing.sm)) Row( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 16.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.md), horizontalArrangement = Arrangement.SpaceBetween ) { - VehicleStatCol( - label = "POWER", - value = "${vehicle.horsepower}", - unit = "hp", - accentColor = TrackProTheme.colors.accentAmber, - textMuted = TrackProTheme.colors.textMuted - ) - VehicleStatCol( - label = "TORQUE", - value = vehicle.torque?.toString() ?: "—", - unit = "Nm", - accentColor = TrackProTheme.colors.accentAmber, - textMuted = TrackProTheme.colors.textMuted - ) - VehicleStatCol( - label = "WEIGHT", - value = "${vehicle.weight}", - unit = "kg", - accentColor = TrackProTheme.colors.accentAmber, - textMuted = TrackProTheme.colors.textMuted - ) + 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) } HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) } @@ -163,29 +130,31 @@ fun CarViewScreen(vehicleId: Long) { modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgElevated) - .padding(horizontal = 24.dp, vertical = 16.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.md), horizontalArrangement = Arrangement.SpaceBetween ) { - VehicleStatCol( - label = "TOP SPEED", + StatCell( + label = "Top Speed", value = vehicle.topSpeed?.let { UnitFormatter.formatSpeed(it, useMetric) } ?: "—", unit = UnitFormatter.speedUnitLabel(useMetric).lowercase(), - accentColor = TrackProTheme.colors.accentAmber, - textMuted = TrackProTheme.colors.textMuted + valueColor = TrackProTheme.colors.accentAmber, + size = StatCellSize.Large, + horizontalAlignment = Alignment.CenterHorizontally ) - VehicleStatCol( + StatCell( label = "0–100", value = vehicle.acceleration?.toString() ?: "—", unit = "sec", - accentColor = TrackProTheme.colors.accentAmber, - textMuted = TrackProTheme.colors.textMuted + valueColor = TrackProTheme.colors.accentAmber, + size = StatCellSize.Large, + horizontalAlignment = Alignment.CenterHorizontally ) - VehicleStatCol( - label = "DRIVETRAIN", + StatCell( + label = "Drivetrain", value = vehicle.drivetrain, - unit = "", - accentColor = TrackProTheme.colors.accentAmber, - textMuted = TrackProTheme.colors.textMuted + valueColor = TrackProTheme.colors.accentAmber, + size = StatCellSize.Large, + horizontalAlignment = Alignment.CenterHorizontally ) } HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) @@ -193,29 +162,29 @@ fun CarViewScreen(vehicleId: Long) { // ── Mechanical details ──────────────── item { - VehicleSectionHeader("MECHANICAL", TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine) + SectionLabel("Mechanical", modifier = Modifier.padding(horizontal = Spacing.lg, vertical = Spacing.sm)) } item { - VehicleInfoRow("ENGINE TYPE", vehicle.engineType, + VehicleInfoRow("Engine Type", vehicle.engineType, TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine, TrackProTheme.colors.bgCard) } item { - VehicleInfoRow("TRANSMISSION", vehicle.transmission, + VehicleInfoRow("Transmission", vehicle.transmission, TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine, TrackProTheme.colors.bgCard) } item { - VehicleInfoRow("FUEL TYPE", vehicle.fuelType, + VehicleInfoRow("Fuel Type", vehicle.fuelType, TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine, TrackProTheme.colors.bgCard) } vehicle.fuelCapacity?.let { item { - VehicleInfoRow("FUEL CAPACITY", "$it L", + VehicleInfoRow("Fuel Capacity", "$it L", TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine, TrackProTheme.colors.bgCard) } } vehicle.suspensionType?.let { item { - VehicleInfoRow("SUSPENSION", it, + VehicleInfoRow("Suspension", it, TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine, TrackProTheme.colors.bgCard) } } @@ -223,52 +192,20 @@ fun CarViewScreen(vehicleId: Long) { // ── Tyres ───────────────────────────── item { HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) - VehicleSectionHeader("TYRES & SETUP", TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine) + SectionLabel("Tyres & Setup", modifier = Modifier.padding(horizontal = Spacing.lg, vertical = Spacing.sm)) } item { - VehicleInfoRow("TYRE TYPE", vehicle.tireType, + VehicleInfoRow("Tyre Type", vehicle.tireType, TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine, TrackProTheme.colors.bgCard) } - item { Spacer(Modifier.height(24.dp)) } + item { Spacer(Modifier.height(Spacing.xl)) } } } } } } -@Composable -private fun VehicleSectionHeader(title: String, textMuted: Color, sectorLine: Color) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF0A0C11)) - .padding(horizontal = 24.dp, vertical = 8.dp) - ) { - Text(title, color = textMuted, fontSize = 9.sp, - fontWeight = FontWeight.Black, letterSpacing = 3.sp) - } - HorizontalDivider(color = sectorLine, thickness = 1.dp) -} - -@Composable -private fun VehicleStatCol( - label: String, - value: String, - unit: String, - accentColor: Color, - textMuted: Color -) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(label, color = textMuted, fontSize = 9.sp, - letterSpacing = 2.sp, fontWeight = FontWeight.Bold) - Text(value, color = accentColor, fontSize = 26.sp, fontWeight = FontWeight.Black) - if (unit.isNotEmpty()) { - Text(unit, color = textMuted, fontSize = 10.sp, fontWeight = FontWeight.Bold) - } - } -} - @Composable private fun VehicleInfoRow( label: String, @@ -286,13 +223,12 @@ private fun VehicleInfoRow( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 14.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.sm), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(label, color = textMuted, fontSize = 10.sp, - letterSpacing = 2.sp, fontWeight = FontWeight.Bold) - Text(value, color = textPrimary, fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(label.uppercase(), style = TrackProType.label, color = textMuted) + Text(value, style = TrackProType.body, color = textPrimary) } HorizontalDivider(color = sectorLine, thickness = 1.dp) } 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 a307c04..5c51967 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 @@ -26,7 +26,6 @@ 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.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -42,6 +41,12 @@ import com.example.trackpro.managerClasses.utilities.SpeedColorUtils import com.example.trackpro.managerClasses.utilities.UnitFormatter import com.example.trackpro.screens.telemetricScreens.DragMetricCard import com.example.trackpro.screens.telemetricScreens.DragMetricDisplay +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.StatCell +import com.example.trackpro.components.StatCellSize +import com.example.trackpro.theme.DataVizColors +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProType import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.data.Entry @@ -205,28 +210,14 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { .fillMaxSize() .background(TrackProTheme.colors.bgDeep) ) { - // ── Top bar ─────────────────────────────────────────── - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentCyan) - .padding(horizontal = 16.dp, vertical = 5.dp) - ) { - Text( - text = "● SESSION OVERVIEW", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) - } + AppTopBar(title = "Session Overview", accent = TrackProTheme.colors.accentCyan, onBack = onBack) // ── Compact stats panel ─────────────────────────────── Column( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 16.dp, vertical = 8.dp), + .padding(horizontal = Spacing.md, vertical = Spacing.sm), verticalArrangement = Arrangement.spacedBy(6.dp) ) { Row( @@ -236,18 +227,12 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { ) { if (coordinates.isNotEmpty()) { val totalTime = coordinates.last().timestamp - coordinates.first().timestamp - CompactStat( - label = "DURATION", - value = formatTime(totalTime), - textMuted = TrackProTheme.colors.textMuted, - valueColor = TrackProTheme.colors.textPrimary - ) + StatCell(label = "Duration", value = formatTime(totalTime), size = StatCellSize.Small) } - CompactStat( - label = "DISTANCE", + StatCell( + label = "Distance", value = if (totalDist <= 0) "—" else UnitFormatter.formatDistance(totalDist, useMetric), - textMuted = TrackProTheme.colors.textMuted, - valueColor = TrackProTheme.colors.textPrimary + size = StatCellSize.Small ) } @@ -294,23 +279,21 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 16.dp, vertical = 6.dp), + .padding(horizontal = Spacing.md, vertical = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( - text = if (showMap) "GPS TRACE" else "SPEED TRACE", - color = TrackProTheme.colors.textMuted, - fontSize = 9.sp, - letterSpacing = 2.sp, - fontWeight = FontWeight.Bold + text = if (showMap) "GPS Trace" else "Speed Trace", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { if (!showMap) { - listOf(true to "METER", false to "SEC").forEach { (isMeters, label) -> + listOf(true to "Meter", false to "Sec").forEach { (isMeters, label) -> val active = xAxisInMeters == isMeters Box( modifier = Modifier @@ -319,18 +302,17 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { RoundedCornerShape(3.dp) ) .clickable { xAxisInMeters = isMeters } - .padding(horizontal = 10.dp, vertical = 3.dp) + .padding(horizontal = 8.dp, vertical = 3.dp) ) { Text( text = label, - color = if (active) Color.Black else TrackProTheme.colors.textMuted, - fontSize = 9.sp, - fontWeight = FontWeight.Black, + style = TrackProType.body.copy(fontSize = 9.sp), + color = if (active) Color.Black else TrackProTheme.colors.textMuted ) } } } - listOf(false to "CHART", true to "MAP").forEach { (isMap, label) -> + listOf(false to "Chart", true to "Map").forEach { (isMap, label) -> val active = showMap == isMap Box( modifier = Modifier @@ -339,13 +321,12 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { RoundedCornerShape(3.dp) ) .clickable { showMap = isMap } - .padding(horizontal = 10.dp, vertical = 3.dp) + .padding(horizontal = 8.dp, vertical = 3.dp) ) { Text( text = label, - color = if (active) Color.Black else TrackProTheme.colors.textMuted, - fontSize = 9.sp, - fontWeight = FontWeight.Black, + style = TrackProType.body.copy(fontSize = 9.sp), + color = if (active) Color.Black else TrackProTheme.colors.textMuted ) } } @@ -364,12 +345,7 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { DragSessionMapView(gpsData = mapGpsData, modifier = Modifier.fillMaxSize()) } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - "NO GPS DATA", - color = TrackProTheme.colors.textMuted, - fontSize = 12.sp, - letterSpacing = 2.sp - ) + Text("No GPS data", style = TrackProType.label, color = TrackProTheme.colors.textFaint) } } } else { @@ -389,9 +365,9 @@ fun GraphScreen(onBack: () -> Unit, sessionId: Long) { setDrawValues(false) setDrawCircles(false) lineWidth = 2f - color = android.graphics.Color.parseColor("#E8001C") + color = android.graphics.Color.parseColor(DataVizColors.chartLine) setDrawFilled(true) - fillColor = android.graphics.Color.parseColor("#E8001C") + fillColor = android.graphics.Color.parseColor(DataVizColors.chartLine) fillAlpha = 40 } chart.data = LineData(dataSet) @@ -488,18 +464,18 @@ private fun drawDragSpeedHeatmap(style: Style, gps: List) { style.addSource(GeoJsonSource("drag-start-src", """{"type":"Feature","geometry":{"type":"Point","coordinates":[${start.longitude},${start.latitude}]}}""")) style.addLayer(CircleLayer("drag-start-layer", "drag-start-src").apply { setProperties( - PropertyFactory.circleColor("#00E676"), + PropertyFactory.circleColor(DataVizColors.startMarker), PropertyFactory.circleRadius(6f), - PropertyFactory.circleStrokeColor("#0E1117"), + PropertyFactory.circleStrokeColor(DataVizColors.darkOutline), PropertyFactory.circleStrokeWidth(1.5f) ) }) style.addSource(GeoJsonSource("drag-end-src", """{"type":"Feature","geometry":{"type":"Point","coordinates":[${end.longitude},${end.latitude}]}}""")) style.addLayer(CircleLayer("drag-end-layer", "drag-end-src").apply { setProperties( - PropertyFactory.circleColor("#FF1744"), + PropertyFactory.circleColor(DataVizColors.endMarker), PropertyFactory.circleRadius(6f), - PropertyFactory.circleStrokeColor("#0E1117"), + PropertyFactory.circleStrokeColor(DataVizColors.darkOutline), PropertyFactory.circleStrokeWidth(1.5f) ) }) @@ -516,12 +492,12 @@ private fun LineChart.setupChartStyle() { xAxis.apply { position = XAxis.XAxisPosition.BOTTOM setDrawGridLines(false) - textColor = android.graphics.Color.parseColor("#6B7280") + textColor = android.graphics.Color.parseColor(DataVizColors.chartAxisText) textSize = 8f } axisLeft.apply { - textColor = android.graphics.Color.parseColor("#6B7280") - gridColor = android.graphics.Color.parseColor("#1E2530") + textColor = android.graphics.Color.parseColor(DataVizColors.chartAxisText) + gridColor = android.graphics.Color.parseColor(DataVizColors.chartGrid) textSize = 8f axisMinimum = 0f } @@ -530,15 +506,7 @@ private fun LineChart.setupChartStyle() { legend.isEnabled = false setNoDataText("Calculating data...") setNoDataTextColor(android.graphics.Color.WHITE) - setBackgroundColor(android.graphics.Color.parseColor("#0E1117")) -} - -@Composable -private fun CompactStat(label: String, value: String, textMuted: Color, valueColor: Color) { - Column { - Text(text = label, color = textMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold, letterSpacing = 1.sp) - Text(text = value, color = valueColor, fontSize = 14.sp, fontWeight = FontWeight.Black) - } + setBackgroundColor(android.graphics.Color.parseColor(DataVizColors.chartBackground)) } @Preview(showBackground = true) 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 04e785b..c9e85a1 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 @@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -37,6 +36,12 @@ import com.example.trackpro.dataClasses.LapInfoData import com.example.trackpro.dataClasses.LapTimeData import com.example.trackpro.dataClasses.SectorTimeData import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.SectionLabel +import com.example.trackpro.theme.DataVizColors +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType import com.example.trackpro.TrackProApp import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.managerClasses.utilities.SpeedColorUtils @@ -155,8 +160,7 @@ fun LapDetailScreen( LoadingView() } else if (primaryLap == null) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("LAP NOT FOUND", color = TrackProTheme.colors.textMuted, - fontSize = 12.sp, letterSpacing = 3.sp) + Text("Lap not found", style = TrackProType.label, color = TrackProTheme.colors.textFaint) } } else { val lap = primaryLap!! @@ -175,57 +179,26 @@ fun LapDetailScreen( .fillMaxWidth() .align(Alignment.TopCenter) ) { - // Status bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentBlue) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - // Back button - Box( - modifier = Modifier - .clip(RoundedCornerShape(4.dp)) - .clickable { navController.popBackStack() } - .background(Color.Black.copy(alpha = 0.15f)) - .padding(horizontal = 8.dp, vertical = 2.dp) - ) { - Text("← BACK", color = Color.Black, fontSize = 9.sp, - fontWeight = FontWeight.Black, letterSpacing = 2.sp) - } - Text( - "LAP ${lap.lapnumber} · ${lap.laptime}", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 2.sp - ) - } + AppTopBar( + title = "Lap ${lap.lapnumber} · ${lap.laptime}", + accent = TrackProTheme.colors.accentBlue, + onBack = { navController.popBackStack() }, + trailing = { Text( - "⚡ ${UnitFormatter.formatSpeed(primaryTopSpeed, useMetric)} ${UnitFormatter.speedUnitLabel(useMetric)}", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.Bold + "${UnitFormatter.formatSpeed(primaryTopSpeed, useMetric)} ${UnitFormatter.speedUnitLabel(useMetric)}", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) } - } + ) // Heatmap mode switcher Row( modifier = Modifier - .padding(horizontal = 16.dp, vertical = 10.dp) + .padding(horizontal = Spacing.md, vertical = Spacing.sm) .background( TrackProTheme.colors.bgCard.copy(alpha = 0.92f), - RoundedCornerShape(12.dp) + RoundedCornerShape(10.dp) ) .padding(4.dp), horizontalArrangement = Arrangement.spacedBy(2.dp) @@ -234,21 +207,19 @@ fun LapDetailScreen( val selected = mode == heatmapMode Box( modifier = Modifier - .clip(RoundedCornerShape(9.dp)) + .clip(RoundedCornerShape(8.dp)) .background( if (selected) TrackProTheme.colors.accentBlue else Color.Transparent ) .clickable { heatmapMode = mode } - .padding(horizontal = 12.dp, vertical = 6.dp), + .padding(horizontal = Spacing.md, vertical = 6.dp), contentAlignment = Alignment.Center ) { Text( - "${mode.icon} ${mode.label}", - color = if (selected) Color.Black else TrackProTheme.colors.textMuted, - fontSize = 9.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + mode.label, + style = TrackProType.label, + color = if (selected) Color.Black else TrackProTheme.colors.textMuted ) } } @@ -278,18 +249,14 @@ fun LapDetailScreen( .background(COMPARE_COLOR, CircleShape) ) Column { - Text("COMPARE · LAP ${cl.lapnumber}", - color = COMPARE_COLOR, fontSize = 8.sp, - fontWeight = FontWeight.Black, letterSpacing = 1.sp) - Text(cl.laptime, - color = TrackProTheme.colors.textPrimary, fontSize = 13.sp, - fontWeight = FontWeight.Bold) + Text("Compare · Lap ${cl.lapnumber}", style = TrackProType.label.copy(fontSize = 8.sp), color = COMPARE_COLOR) + Text(cl.laptime, style = TrackProType.titleMedium.copy(fontSize = 13.sp), color = TrackProTheme.colors.textPrimary) if (compareMs > 0) { val sign = if (deltaMs > 0) "+" else "" Text( text = "${sign}${deltaMs.toLapTimeString()}", - color = if (deltaMs < 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad, - fontSize = 10.sp, fontWeight = FontWeight.Bold + style = TrackProType.body.copy(fontSize = 10.sp), + color = if (deltaMs < 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad ) } } @@ -340,21 +307,19 @@ fun LapDetailScreen( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard.copy(alpha = 0.95f)) - .padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp) + .padding(horizontal = Spacing.md, vertical = Spacing.sm), + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { // Stats toggle ActionButton( - label = if (showStatsPanel) "HIDE STATS" else "STATS", - icon = "📊", + label = if (showStatsPanel) "Hide Stats" else "Stats", color = TrackProTheme.colors.accentBlue, modifier = Modifier.weight(1f) ) { showStatsPanel = !showStatsPanel } // Compare toggle ActionButton( - label = if (compareLap != null) "COMPARING" else "COMPARE", - icon = "⚖", + label = if (compareLap != null) "Comparing" else "Compare", color = if (compareLap != null) COMPARE_COLOR else TrackProTheme.colors.accentAmber, modifier = Modifier.weight(1f) ) { showLapPicker = true } @@ -384,8 +349,11 @@ fun LapDetailScreen( // ── Heatmap Map View ─────────────────────────────────────── -private val PRIMARY_COLOR = Color(0xFF8989FF) // AccentGreen from theme -private val COMPARE_COLOR = Color(0xFF5BB6A8) // AccentAmber from theme +// 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) @Composable fun LapHeatmapMapView( @@ -466,8 +434,8 @@ private fun drawAllLayers( } // Draw start/end dots - drawEndpointDot(style, primaryGps.first(), "start", "#00E676") - drawEndpointDot(style, primaryGps.last(), "end", "#FF1744") + drawEndpointDot(style, primaryGps.first(), "start", DataVizColors.startMarker) + drawEndpointDot(style, primaryGps.last(), "end", DataVizColors.endMarker) } /** Draws a thin segmented line where each segment is colored by speed interpolation */ @@ -625,12 +593,11 @@ private fun StatsPanel( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 8.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.sm), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text("LAP STATS", color = TrackProTheme.colors.textMuted, fontSize = 9.sp, - fontWeight = FontWeight.Black, letterSpacing = 3.sp) + SectionLabel("Lap Stats") Icon( Icons.Default.Close, "dismiss", tint = TrackProTheme.colors.textMuted, @@ -644,11 +611,10 @@ private fun StatsPanel( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 10.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.sm), horizontalArrangement = Arrangement.SpaceBetween ) { - Text("METRIC", color = TrackProTheme.colors.textMuted, fontSize = 8.sp, - fontWeight = FontWeight.Black, letterSpacing = 2.sp, + Text("Metric", style = TrackProType.label.copy(fontSize = 8.sp), color = TrackProTheme.colors.textMuted, modifier = Modifier.weight(1.4f)) Row( modifier = Modifier.weight(1f), @@ -656,8 +622,7 @@ private fun StatsPanel( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Box(Modifier.size(6.dp).background(PRIMARY_COLOR, CircleShape)) - Text("LAP ${primaryLap.lapnumber}", color = PRIMARY_COLOR, fontSize = 8.sp, - fontWeight = FontWeight.Black, letterSpacing = 1.sp) + Text("Lap ${primaryLap.lapnumber}", style = TrackProType.label.copy(fontSize = 8.sp), color = PRIMARY_COLOR) } if (compareLap != null) { Row( @@ -666,8 +631,7 @@ private fun StatsPanel( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Box(Modifier.size(6.dp).background(COMPARE_COLOR, CircleShape)) - Text("LAP ${compareLap.lapnumber}", color = COMPARE_COLOR, fontSize = 8.sp, - fontWeight = FontWeight.Black, letterSpacing = 1.sp) + Text("Lap ${compareLap.lapnumber}", style = TrackProType.label.copy(fontSize = 8.sp), color = COMPARE_COLOR) } } } @@ -676,24 +640,24 @@ private fun StatsPanel( val speedUnit = UnitFormatter.speedUnitLabel(useMetric) val rows = buildList { - add(Triple("LAP TIME", primaryLap.laptime, compareLap?.laptime ?: "—")) - add(Triple("TOP SPEED", "${UnitFormatter.formatSpeed(primaryTopSpeed, useMetric)} $speedUnit", + add(Triple("Lap Time", primaryLap.laptime, compareLap?.laptime ?: "—")) + add(Triple("Top Speed", "${UnitFormatter.formatSpeed(primaryTopSpeed, useMetric)} $speedUnit", if (compareLap != null) "${UnitFormatter.formatSpeed(compareTopSpeed, useMetric)} $speedUnit" else "—")) - add(Triple("AVG SPEED", "${UnitFormatter.formatSpeedPrecise(primaryAvgSpd.toDouble(), useMetric)} $speedUnit", + add(Triple("Avg Speed", "${UnitFormatter.formatSpeedPrecise(primaryAvgSpd.toDouble(), useMetric)} $speedUnit", if (compareLap != null) "${UnitFormatter.formatSpeedPrecise(compareAvgSpd.toDouble(), useMetric)} $speedUnit" else "—")) - add(Triple("GPS POINTS", "${primaryGps.size}", + add(Triple("GPS Points", "${primaryGps.size}", if (compareLap != null) "${compareGps.size}" else "—")) primarySectors.sortedBy { it.sectorIndex }.forEach { sector -> val compareSplit = compareSectors.find { it.sectorIndex == sector.sectorIndex } add(Triple( - "SECTOR ${sector.sectorIndex + 1}", + "Sector ${sector.sectorIndex + 1}", String.format("%.2fs", sector.splitTimeMs / 1000.0), if (compareLap != null) compareSplit?.let { String.format("%.2fs", it.splitTimeMs / 1000.0) } ?: "—" else "—" )) } if (compareLap != null) { val sign = if (deltaMs > 0) "+" else "" - add(Triple("DELTA", "${sign}${deltaMs.toLapTimeString()}", "")) + add(Triple("Delta", "${sign}${deltaMs.toLapTimeString()}", "")) } } @@ -705,21 +669,17 @@ private fun StatsPanel( if (i % 2 == 0) Color.Transparent else TrackProTheme.colors.bgElevated.copy(alpha = 0.5f) ) - .padding(horizontal = 20.dp, vertical = 10.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.sm), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(label, color = TrackProTheme.colors.textMuted, fontSize = 9.sp, - fontWeight = FontWeight.Bold, letterSpacing = 2.sp, + Text(label.uppercase(), style = TrackProType.label.copy(fontSize = 9.sp), color = TrackProTheme.colors.textMuted, modifier = Modifier.weight(1.4f)) - Text(v1, color = if (label == "DELTA" && deltaMs < 0) TrackProTheme.colors.deltaGood + Text(v1, style = TrackProType.body.copy(fontSize = 13.sp), color = if (label == "Delta" && deltaMs < 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.textPrimary, - fontSize = 13.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) if (compareLap != null && v2.isNotEmpty()) { - Text(v2, color = if (label == "DELTA") TrackProTheme.colors.accentCyan - else TrackProTheme.colors.textPrimary, - fontSize = 13.sp, fontWeight = FontWeight.Bold, + Text(v2, style = TrackProType.body.copy(fontSize = 13.sp), color = TrackProTheme.colors.textPrimary, modifier = Modifier.weight(1f)) } } @@ -728,7 +688,7 @@ private fun StatsPanel( } } - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(Spacing.sm)) } } @@ -765,18 +725,17 @@ private fun LapPickerSheet( Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 8.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.sm), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text("SELECT LAP TO COMPARE", color = TrackProTheme.colors.textMuted, fontSize = 9.sp, - fontWeight = FontWeight.Black, letterSpacing = 3.sp) + SectionLabel("Select Lap to Compare") Icon(Icons.Default.Close, "close", tint = TrackProTheme.colors.textMuted, modifier = Modifier.size(16.dp).clickable { onDismiss() }) } HorizontalDivider(color = TrackProTheme.colors.sectorLine) - LazyColumn(contentPadding = PaddingValues(vertical = 4.dp, horizontal = 12.dp)) { + LazyColumn(contentPadding = PaddingValues(vertical = 4.dp, horizontal = Spacing.sm)) { items(laps) { lap -> val lapMs = lap.laptime.toLapTimeMillis() val deltaMs = lapMs - primaryLapMs @@ -805,44 +764,41 @@ private fun LapPickerSheet( shape = RoundedCornerShape(8.dp) ) .clickable { onSelect(lap) } - .padding(horizontal = 14.dp, vertical = 10.dp), + .padding(horizontal = Spacing.md, vertical = Spacing.sm), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { Text( String.format("%02d", lap.lapnumber), - color = if (isBest) TrackProTheme.colors.accentBlue else TrackProTheme.colors.textPrimary, - fontSize = 18.sp, fontWeight = FontWeight.Black + style = TrackProType.statValue.copy(fontSize = 16.sp), + color = if (isBest) TrackProTheme.colors.accentBlue else TrackProTheme.colors.textPrimary ) if (isBest) { Box( Modifier - .background(TrackProTheme.colors.accentBlue.copy(alpha = 0.15f), - RoundedCornerShape(3.dp)) + .background(TrackProTheme.colors.accentBlue.copy(alpha = 0.15f), TrackProShapes.badge) .padding(horizontal = 5.dp, vertical = 2.dp) ) { - Text("BEST", color = TrackProTheme.colors.accentBlue, fontSize = 7.sp, - fontWeight = FontWeight.Black, letterSpacing = 1.sp) + Text("Best", style = TrackProType.label.copy(fontSize = 7.sp), color = TrackProTheme.colors.accentBlue) } } } Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { // Delta vs primary val sign = if (deltaMs > 0) "+" else "" Text( "${sign}${deltaMs.toLapTimeString()}", - color = if (deltaMs < 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad, - fontSize = 11.sp, fontWeight = FontWeight.Bold + style = TrackProType.body.copy(fontSize = 11.sp), + color = if (deltaMs < 0) TrackProTheme.colors.deltaGood else TrackProTheme.colors.deltaBad ) - Text(lap.laptime, color = TrackProTheme.colors.textPrimary, - fontSize = 15.sp, fontWeight = FontWeight.Bold) + Text(lap.laptime, style = TrackProType.titleMedium.copy(fontSize = 15.sp), color = TrackProTheme.colors.textPrimary) if (isSelected) { Box(Modifier.size(8.dp).background(COMPARE_COLOR, CircleShape)) } @@ -850,7 +806,7 @@ private fun LapPickerSheet( } } } - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(Spacing.sm)) } } @@ -866,8 +822,7 @@ private fun LoadingView() { strokeWidth = 2.dp ) Spacer(Modifier.height(12.dp)) - Text("LOADING LAP", color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, letterSpacing = 3.sp) + Text("Loading lap", style = TrackProType.label, color = TrackProTheme.colors.textFaint) } } } @@ -877,28 +832,20 @@ private fun LoadingView() { @Composable private fun ActionButton( label: String, - icon: String, color: Color, modifier: Modifier = Modifier, onClick: () -> Unit ) { Box( modifier = modifier - .clip(RoundedCornerShape(10.dp)) + .clip(RoundedCornerShape(8.dp)) .background(color.copy(alpha = 0.12f)) - .border(1.dp, color.copy(alpha = 0.3f), RoundedCornerShape(10.dp)) + .border(1.dp, color.copy(alpha = 0.3f), RoundedCornerShape(8.dp)) .clickable { onClick() } - .padding(vertical = 10.dp), + .padding(vertical = Spacing.sm), contentAlignment = Alignment.Center ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - Text(icon, fontSize = 14.sp) - Text(label, color = color, fontSize = 10.sp, - fontWeight = FontWeight.Black, letterSpacing = 1.5.sp) - } + Text(label, style = TrackProType.titleMedium.copy(fontSize = 12.sp), color = color) } } diff --git a/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/TimeAttackListItem.kt b/app/src/main/java/com/example/trackpro/screens/listViewScreens/listItems/TimeAttackListItem.kt index ef25a63..8a0e4d6 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 @@ -35,7 +35,6 @@ 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.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -48,6 +47,13 @@ import com.example.trackpro.dataClasses.LapTimeData import com.example.trackpro.dataClasses.SessionData import com.example.trackpro.dataClasses.VehicleInformationData import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.SectionLabel +import com.example.trackpro.components.StatCell +import com.example.trackpro.components.StatCellSize +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType import com.example.trackpro.managerClasses.ESPDatabase import com.example.trackpro.managerClasses.utilities.DateFormatterUtil import com.example.trackpro.managerClasses.utilities.UnitFormatter @@ -122,14 +128,12 @@ fun TimeAttackListItemScreen( CircularProgressIndicator(color = TrackProTheme.colors.accentBlue, modifier = Modifier.size(36.dp), strokeWidth = 2.dp) Spacer(Modifier.height(12.dp)) - Text("LOADING SESSION", color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, letterSpacing = 3.sp) + Text("Loading session", style = TrackProType.label, color = TrackProTheme.colors.textFaint) } } } else if (sessionData == null) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("SESSION NOT FOUND", color = TrackProTheme.colors.textMuted, - fontSize = 12.sp, letterSpacing = 3.sp) + Text("Session not found", style = TrackProType.label, color = TrackProTheme.colors.textFaint) } } else { val session = sessionData!! @@ -169,8 +173,8 @@ fun TimeAttackListItemScreen( } } else "—" val trendColor = when { - trend.contains("IMPROVING") -> TrackProTheme.colors.accentBlue - trend.contains("FADING") ->TrackProTheme.colors.accentCyan + trend.contains("IMPROVING") -> TrackProTheme.colors.deltaGood + trend.contains("FADING") -> TrackProTheme.colors.deltaBad else -> TrackProTheme.colors.accentAmber } @@ -180,23 +184,13 @@ fun TimeAttackListItemScreen( ) { // ── Top bar item { - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentBlue) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text("● SESSION DETAIL", color = Color.Black, fontSize = 11.sp, - fontWeight = FontWeight.Black, letterSpacing = 3.sp) - Text("${lapTimes.size} LAPS", color = Color.Black, - fontSize = 10.sp, fontWeight = FontWeight.Bold, letterSpacing = 2.sp) + AppTopBar( + title = "Session Detail", + accent = TrackProTheme.colors.accentBlue, + trailing = { + Text("${lapTimes.size} laps", style = TrackProType.label, color = TrackProTheme.colors.textMuted) } - } + ) } // ── Session + vehicle header @@ -205,34 +199,31 @@ fun TimeAttackListItemScreen( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 16.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.md), verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( text = session.eventType, - color = TrackProTheme.colors.textPrimary, - fontSize = 20.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-0.5).sp + style = TrackProType.titleLarge, + color = TrackProTheme.colors.textPrimary ) if (vehicle != null) { Text( text = "${vehicle.manufacturer} ${vehicle.model} (${vehicle.year})", - color = TrackProTheme.colors.textMuted, - fontSize = 13.sp, - fontWeight = FontWeight.Bold + style = TrackProType.body, + color = TrackProTheme.colors.textMuted ) Text( text = "${vehicle.engineType} · ${vehicle.horsepower}hp · ${vehicle.drivetrain}", - color = TrackProTheme.colors.textMuted.copy(alpha = 0.7f), - fontSize = 11.sp + style = TrackProType.body.copy(fontSize = 11.sp), + color = TrackProTheme.colors.textMuted.copy(alpha = 0.7f) ) } Spacer(Modifier.height(4.dp)) Text( text = DateFormatterUtil.getDateTimeFormat().format(Date(session.startTime)), - color = TrackProTheme.colors.textMuted, - fontSize = 11.sp + style = TrackProType.body.copy(fontSize = 11.sp), + color = TrackProTheme.colors.textMuted ) } HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) @@ -240,67 +231,72 @@ fun TimeAttackListItemScreen( // ── Key performance metrics item { - SectionHeader("KEY METRICS", TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine) + SectionLabel("Key Metrics", modifier = Modifier.padding(horizontal = Spacing.lg, vertical = Spacing.sm)) Row( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 16.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.md), horizontalArrangement = Arrangement.SpaceBetween ) { - MetricColumn("BEST LAP", bestLap?.laptime ?: "—", TrackProTheme.colors.accentBlue, TrackProTheme.colors.textMuted) - MetricColumn("AVERAGE", avgMs.toLapTimeString(), TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted) - MetricColumn("WORST", worstMs.toLapTimeString(), - if (lapMillis.size > 1) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textPrimary, TrackProTheme.colors.textMuted) + StatCell(label = "Best Lap", value = bestLap?.laptime ?: "—", valueColor = TrackProTheme.colors.deltaGood, size = StatCellSize.Large, horizontalAlignment = Alignment.CenterHorizontally) + StatCell(label = "Average", value = avgMs.toLapTimeString(), size = StatCellSize.Large, horizontalAlignment = Alignment.CenterHorizontally) + StatCell( + label = "Worst", + value = worstMs.toLapTimeString(), + valueColor = if (lapMillis.size > 1) TrackProTheme.colors.deltaBad else TrackProTheme.colors.textPrimary, + size = StatCellSize.Large, + horizontalAlignment = Alignment.CenterHorizontally + ) } HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) } // ── Session stats row item { - SectionHeader("SESSION STATS", TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine) + SectionLabel("Session Stats", modifier = Modifier.padding(horizontal = Spacing.lg, vertical = Spacing.sm)) Column( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + .padding(horizontal = Spacing.lg, vertical = Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { StatRowItem( - label = "TOTAL SESSION TIME", + label = "Total Session Time", value = sessionDuration.toLapTimeString(), textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted ) StatRowItem( - label = "TOP SPEED (SESSION)", + label = "Top Speed (Session)", value = "${UnitFormatter.formatSpeedPrecise(topSpeedOverall.toDouble(), useMetric)} ${UnitFormatter.speedUnitLabel(useMetric)}", textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted ) StatRowItem( - label = "LAP COUNT", + label = "Lap Count", value = "${lapTimes.size}", textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted ) StatRowItem( - label = "CONSISTENCY (σ)", + label = "Consistency (σ)", value = consistency, textPrimary = if (consistency != "—" && consistency.replace("%","").toDoubleOrNull()?.let { it < 1.0 } == true) - TrackProTheme.colors.accentBlue else TrackProTheme.colors.textPrimary, + TrackProTheme.colors.deltaGood else TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted ) StatRowItem( - label = "GAP: BEST → WORST", + label = "Gap: Best → Worst", value = if (lapMillis.size > 1) "+${(worstMs - bestMs).toLapTimeString()}" else "—", textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted ) StatRowItem( - label = "PERFORMANCE TREND", + label = "Performance Trend", value = trend, textPrimary = trendColor, textMuted = TrackProTheme.colors.textMuted @@ -311,7 +307,7 @@ fun TimeAttackListItemScreen( // ── Lap-by-lap breakdown item { - SectionHeader("LAP BREAKDOWN", TrackProTheme.colors.textMuted, TrackProTheme.colors.sectorLine) + SectionLabel("Lap Breakdown", modifier = Modifier.padding(horizontal = Spacing.lg, vertical = Spacing.sm)) } if (lapTimes.isEmpty()) { @@ -319,11 +315,10 @@ fun TimeAttackListItemScreen( Box( modifier = Modifier .fillMaxWidth() - .padding(32.dp), + .padding(Spacing.xl), contentAlignment = Alignment.Center ) { - Text("NO LAPS RECORDED", color = TrackProTheme.colors.textMuted, - fontSize = 11.sp, letterSpacing = 3.sp) + Text("No laps recorded", style = TrackProType.label, color = TrackProTheme.colors.textFaint) } } } else { @@ -345,8 +340,8 @@ fun TimeAttackListItemScreen( useMetric = useMetric, bgCard = TrackProTheme.colors.bgCard, bgElevated = TrackProTheme.colors.bgElevated, - accentGreen = TrackProTheme.colors.accentBlue, - accentRed = TrackProTheme.colors.accentCyan, + goodColor = TrackProTheme.colors.deltaGood, + badColor = TrackProTheme.colors.deltaBad, accentAmber = TrackProTheme.colors.accentAmber, textPrimary = TrackProTheme.colors.textPrimary, textMuted = TrackProTheme.colors.textMuted, @@ -372,104 +367,91 @@ private fun LapRow( useMetric: Boolean, bgCard: Color, bgElevated: Color, - accentGreen: Color, - accentRed: Color, + goodColor: Color, + badColor: Color, accentAmber: Color, textPrimary: Color, textMuted: Color, sectorLine: Color ) { val accentColor = when { - isBest -> accentGreen - isWorst -> accentRed + isBest -> goodColor + isWorst -> badColor else -> textMuted } val badge = when { - isBest -> "BEST" - isWorst -> "SLOW" - else -> "LAP" + isBest -> "Best" + isWorst -> "Slow" + else -> "Lap" } Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp) + .padding(horizontal = Spacing.md, vertical = 4.dp) .background( - if (isBest) accentGreen.copy(alpha = 0.05f) else bgCard, - RoundedCornerShape(8.dp) + if (isBest) goodColor.copy(alpha = 0.05f) else bgCard, + TrackProShapes.card ) .border( width = if (isBest) 1.dp else 0.dp, - color = if (isBest) accentGreen.copy(alpha = 0.3f) else Color.Transparent, - shape = RoundedCornerShape(8.dp) + color = if (isBest) goodColor.copy(alpha = 0.3f) else Color.Transparent, + shape = TrackProShapes.card ) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(horizontal = Spacing.md, vertical = Spacing.sm), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { // Left: lap number + badge Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { Text( text = String.format("%02d", lap.lapnumber), - color = accentColor, - fontSize = 22.sp, - fontWeight = FontWeight.Black + style = TrackProType.statValue.copy(fontSize = 18.sp), + color = accentColor ) Box( modifier = Modifier - .background(accentColor.copy(alpha = 0.15f), RoundedCornerShape(3.dp)) + .background(accentColor.copy(alpha = 0.15f), TrackProShapes.badge) .padding(horizontal = 6.dp, vertical = 2.dp) ) { - Text(badge, color = accentColor, fontSize = 8.sp, - fontWeight = FontWeight.Black, letterSpacing = 1.sp) + Text(badge.uppercase(), style = TrackProType.label.copy(fontSize = 8.sp), color = accentColor) } } // Center: top speed - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("TOP SPEED", color = textMuted, fontSize = 8.sp, - letterSpacing = 1.sp, fontWeight = FontWeight.Bold) - Text( - text = if (topSpeed > 0) UnitFormatter.formatSpeed(topSpeed, useMetric) else "—", - color = textPrimary, - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - if (topSpeed > 0) { - Text(UnitFormatter.speedUnitLabel(useMetric).lowercase(), color = textMuted, fontSize = 8.sp) - } - } + StatCell( + label = "Top Speed", + value = if (topSpeed > 0) UnitFormatter.formatSpeed(topSpeed, useMetric) else "—", + unit = if (topSpeed > 0) UnitFormatter.speedUnitLabel(useMetric).lowercase() else null, + size = StatCellSize.Small, + horizontalAlignment = Alignment.CenterHorizontally + ) // Right: lap time + delta Column(horizontalAlignment = Alignment.End) { Text( text = lap.laptime, - color = accentColor, - fontSize = 20.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-0.5).sp + style = TrackProType.statValue.copy(fontSize = 17.sp), + color = accentColor ) if (!isBest && deltaMs > 0) { Text( text = "+${deltaMs.toLapTimeString()}", - color = accentRed.copy(alpha = 0.8f), - fontSize = 11.sp, - fontWeight = FontWeight.Bold + style = TrackProType.body.copy(fontSize = 11.sp), + color = badColor.copy(alpha = 0.8f) ) } else if (isBest) { Text( - text = "REFERENCE", - color = accentGreen.copy(alpha = 0.7f), - fontSize = 9.sp, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp + text = "Reference", + style = TrackProType.label.copy(fontSize = 9.sp), + color = goodColor.copy(alpha = 0.7f) ) } } @@ -479,43 +461,13 @@ private fun LapRow( Box( modifier = Modifier .align(Alignment.CenterStart) - .width(3.dp) - .height(40.dp) + .width(2.dp) + .height(36.dp) .background(accentColor, RoundedCornerShape(topEnd = 2.dp, bottomEnd = 2.dp)) ) } } -// ── Shared sub-components ────────────────────────────────── - -@Composable -private fun SectionHeader(title: String, textMuted: Color, sectorLine: Color) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF0A0C11)) - .padding(horizontal = 24.dp, vertical = 8.dp) - ) { - Text( - text = title, - color = textMuted, - fontSize = 9.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) - } - HorizontalDivider(color = sectorLine, thickness = 1.dp) -} - -@Composable -private fun MetricColumn(label: String, value: String, valueColor: Color, textMuted: Color) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(label, color = textMuted, fontSize = 9.sp, - letterSpacing = 1.sp, fontWeight = FontWeight.Bold) - Text(value, color = valueColor, fontSize = 19.sp, fontWeight = FontWeight.Black) - } -} - @Composable private fun StatRowItem(label: String, value: String, textPrimary: Color, textMuted: Color) { Row( @@ -523,9 +475,8 @@ private fun StatRowItem(label: String, value: String, textPrimary: Color, textMu horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(label, color = textMuted, fontSize = 10.sp, - letterSpacing = 2.sp, fontWeight = FontWeight.Bold) - Text(value, color = textPrimary, fontSize = 15.sp, fontWeight = FontWeight.Bold) + Text(label.uppercase(), style = TrackProType.label, color = textMuted) + Text(value, style = TrackProType.body, color = textPrimary) } } 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 261cf45..c647cc2 100644 --- a/app/src/main/java/com/example/trackpro/screens/telemetricScreens/DragScreen.kt +++ b/app/src/main/java/com/example/trackpro/screens/telemetricScreens/DragScreen.kt @@ -1,7 +1,6 @@ package com.example.trackpro.screens.telemetricScreens import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown @@ -40,7 +38,6 @@ 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.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -65,6 +62,14 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter import androidx.core.graphics.toColorInt import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppCard +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.PrimaryButton +import com.example.trackpro.components.SectionLabel +import com.example.trackpro.theme.DataVizColors +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType data class DragMetricDisplay( val label: String, @@ -148,45 +153,29 @@ fun DragRaceScreen( ) { // 1. TOP STATUS BAR - Box( - Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentCyan) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Row( - Modifier.fillMaxWidth(), - Arrangement.SpaceBetween, - Alignment.CenterVertically - ) { - Text( - "● DRAG MODE", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 2.sp - ) + AppTopBar( + title = "Drag Mode", + accent = TrackProTheme.colors.accentCyan, + trailing = { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { if (isSessionActive) { Text( - "⏱ $elapsedTime", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Bold + elapsedTime, + style = TrackProType.statValue.copy(fontSize = 13.sp), + color = TrackProTheme.colors.accentCyan ) } Text( - if (isConnected) "GPS LOCKED" else "GPS SEARCHING", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.Bold + if (isConnected) "GPS Locked" else "GPS Searching", + style = TrackProType.label, + color = if (isConnected) TrackProTheme.colors.deltaGood else TrackProTheme.colors.textFaint ) } } - } + ) Column( Modifier @@ -196,14 +185,11 @@ fun DragRaceScreen( // 2. VEHICLE SELECTOR if (!isSessionActive) { - Box( - Modifier + AppCard( + modifier = Modifier .fillMaxWidth() - .padding(16.dp) - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(12.dp)) - .border(1.dp, TrackProTheme.colors.sectorLine, RoundedCornerShape(12.dp)) + .padding(Spacing.md) .clickable { showVehicleDropdown = true } - .padding(16.dp) ) { Row( Modifier.fillMaxWidth(), @@ -211,29 +197,22 @@ fun DragRaceScreen( verticalAlignment = Alignment.CenterVertically ) { Column(Modifier.weight(1f)) { - Text( - "VEHICLE", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - letterSpacing = 1.sp - ) + Text("Vehicle", style = TrackProType.label, color = TrackProTheme.colors.textMuted) Spacer(Modifier.height(4.dp)) Text( selectedVehicle?.let { "${it.manufacturer} ${it.model}" } ?: "Select Vehicle", + style = TrackProType.titleMedium, color = selectedVehicle?.let { TrackProTheme.colors.textPrimary } ?: TrackProTheme.colors.textMuted.copy(alpha = 0.5f), - fontSize = 16.sp, - fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis ) selectedVehicle?.let { Text( "${it.horsepower}hp · ${it.drivetrain} · ${it.year}", - color = TrackProTheme.colors.textMuted, - fontSize = 11.sp + style = TrackProType.body.copy(fontSize = 11.sp), + color = TrackProTheme.colors.textMuted ) } } @@ -255,13 +234,13 @@ fun DragRaceScreen( Column { Text( "${vehicle.manufacturer} ${vehicle.model}", - color = TrackProTheme.colors.textPrimary, - fontWeight = FontWeight.Bold + style = TrackProType.body, + color = TrackProTheme.colors.textPrimary ) Text( "${vehicle.horsepower}hp · ${vehicle.year}", - color = TrackProTheme.colors.textMuted, - fontSize = 12.sp + style = TrackProType.body.copy(fontSize = 12.sp), + color = TrackProTheme.colors.textMuted ) } }, @@ -278,45 +257,37 @@ fun DragRaceScreen( Box( Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) + .padding(horizontal = Spacing.md, vertical = Spacing.sm) .background( TrackProTheme.colors.bgCard.copy(alpha = 0.5f), - RoundedCornerShape(8.dp) + TrackProShapes.control ) - .padding(12.dp) + .padding(Spacing.sm) ) { selectedVehicle?.let { Text( "${it.manufacturer} ${it.model} · ${it.horsepower}hp", - color = TrackProTheme.colors.textPrimary, - fontSize = 12.sp, - fontWeight = FontWeight.Bold + style = TrackProType.body, + color = TrackProTheme.colors.textPrimary ) } } } // 3. CURRENT SPEED (BIG) - Box( - Modifier + AppCard( + modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(12.dp)) - .border( - 2.dp, - if (isSessionActive) TrackProTheme.colors.accentCyan.copy(alpha = 0.5f) - else TrackProTheme.colors.sectorLine, - RoundedCornerShape(12.dp) - ) - .padding(20.dp) + .padding(horizontal = Spacing.md), + padding = Spacing.lg, + borderColor = if (isSessionActive) TrackProTheme.colors.accentCyan.copy(alpha = 0.5f) + else TrackProTheme.colors.sectorLine ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - "CURRENT SPEED", - color = TrackProTheme.colors.textMuted, - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - letterSpacing = 2.sp + "Current Speed", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) Spacer(Modifier.height(8.dp)) Row( @@ -325,18 +296,15 @@ fun DragRaceScreen( ) { Text( gpsData?.speed?.let { UnitFormatter.formatSpeed(it, useMetric) } ?: "0", - fontSize = 72.sp, - fontWeight = FontWeight.Black, + style = TrackProType.displayNumeric, color = if (isSessionActive) TrackProTheme.colors.accentCyan - else TrackProTheme.colors.textPrimary, - letterSpacing = (-2).sp + else TrackProTheme.colors.textPrimary ) Text( UnitFormatter.speedUnitLabel(useMetric), - fontSize = 16.sp, - fontWeight = FontWeight.Bold, + style = TrackProType.body.copy(fontSize = 15.sp), color = TrackProTheme.colors.textMuted, - modifier = Modifier.padding(bottom = 12.dp) + modifier = Modifier.padding(bottom = 8.dp) ) } @@ -344,9 +312,8 @@ fun DragRaceScreen( Spacer(Modifier.height(8.dp)) Text( "MAX: ${UnitFormatter.formatSpeed(currentMetrics.maxSpeed, useMetric)} ${UnitFormatter.speedUnitLabel(useMetric)} · DIST: ${UnitFormatter.formatDistance(currentMetrics.totalDistance.toDouble(), useMetric)}", - color = TrackProTheme.colors.textMuted, - fontSize = 13.sp, - fontWeight = FontWeight.Bold + style = TrackProType.body.copy(fontSize = 12.sp), + color = TrackProTheme.colors.textMuted ) } } @@ -409,21 +376,15 @@ fun DragRaceScreen( Column( Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(horizontal = Spacing.md), + verticalArrangement = Arrangement.spacedBy(Spacing.sm) ) { - Text( - "PERFORMANCE METRICS", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 2.sp - ) + SectionLabel("Performance Metrics") metrics.chunked(2).forEach { row -> Row( Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(Spacing.sm) ) { row.forEach { metric -> DragMetricCard(metric, Modifier.weight(1f)) @@ -432,26 +393,17 @@ fun DragRaceScreen( } } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(Spacing.md)) // 5. SPEED CHART (LARGE) - Box( - Modifier + AppCard( + modifier = Modifier .fillMaxWidth() .height(300.dp) - .padding(horizontal = 16.dp) - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(12.dp)) - .border(1.dp, TrackProTheme.colors.sectorLine, RoundedCornerShape(12.dp)) - .padding(12.dp) + .padding(horizontal = Spacing.md) ) { Column { - Text( - "SPEED PROFILE", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 2.sp - ) + SectionLabel("Speed Profile") Spacer(Modifier.height(8.dp)) AndroidView( @@ -461,11 +413,11 @@ fun DragRaceScreen( legend.isEnabled = false xAxis.position = XAxis.XAxisPosition.BOTTOM xAxis.setDrawGridLines(true) - xAxis.gridColor = "#1E2530".toColorInt() - xAxis.textColor = "#6B7280".toColorInt() - axisLeft.textColor = "#F0F2F5".toColorInt() + xAxis.gridColor = DataVizColors.chartGrid.toColorInt() + xAxis.textColor = DataVizColors.chartAxisText.toColorInt() + axisLeft.textColor = DataVizColors.chartAxisText.toColorInt() axisLeft.setDrawGridLines(true) - axisLeft.gridColor = "#1E2530".toColorInt() + axisLeft.gridColor = DataVizColors.chartGrid.toColorInt() axisLeft.axisMinimum = 0f axisRight.isEnabled = false setTouchEnabled(true) @@ -478,13 +430,13 @@ fun DragRaceScreen( update = { chart -> if (speedDataPoints.isNotEmpty()) { val dataSet = LineDataSet(speedDataPoints.toList(), "Speed").apply { - color = "#E8001C".toColorInt() + color = DataVizColors.chartLine.toColorInt() lineWidth = 3f setDrawCircles(false) setDrawValues(false) mode = LineDataSet.Mode.CUBIC_BEZIER setDrawFilled(true) - fillColor = "#E8001C".toColorInt() + fillColor = DataVizColors.chartLine.toColorInt() fillAlpha = 40 } chart.data = LineData(dataSet) @@ -504,87 +456,59 @@ fun DragRaceScreen( Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgDeep) - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) + .padding(Spacing.md), + horizontalArrangement = Arrangement.spacedBy(Spacing.md) ) { - Box( - Modifier - .weight(1f) - .height(56.dp) - .background( - when { - isSessionActive -> TrackProTheme.colors.bgElevated - selectedVehicle == null -> TrackProTheme.colors.bgElevated.copy(alpha = 0.3f) - else -> TrackProTheme.colors.accentCyan - }, - RoundedCornerShape(12.dp) - ) - .border( - 2.dp, - when { - isSessionActive -> TrackProTheme.colors.accentCyan - selectedVehicle == null -> TrackProTheme.colors.sectorLine - else -> Color.Transparent - }, - RoundedCornerShape(12.dp) - ) - .clickable(enabled = selectedVehicle != null) { - scope.launch { - if (!isSessionActive && selectedVehicle != null) { - // Start session - val eventType = - "Drag - ${ - LocalDateTime - .now() - .format(DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm")) - }" - sessionID = withContext(Dispatchers.IO) { - sessionManager.startSession( - eventType = eventType, - vehicleId = selectedVehicle!!.vehicleId, - trackId = null - ) - } - isSessionActive = true - sessionStartTime = System.currentTimeMillis() - - // Reset calculator - dragCalculator.resetRealtimeTracking() - currentMetrics = DragMetrics() - speedDataPoints.clear() - chartIndex = 0f - - } else if (isSessionActive) { - // End session - isSessionActive = false - withContext(Dispatchers.IO) { - sessionManager.endSession() - - // Save buffered GPS data - database.rawGPSDataDao().insertAll(dataBuffer) - dataBuffer.clear() - } + PrimaryButton( + text = when { + selectedVehicle == null -> "Select Vehicle First" + isSessionActive -> "Stop Session" + else -> "Start Drag" + }, + onClick = { + scope.launch { + if (!isSessionActive && selectedVehicle != null) { + // Start session + val eventType = + "Drag - ${ + LocalDateTime + .now() + .format(DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm")) + }" + sessionID = withContext(Dispatchers.IO) { + sessionManager.startSession( + eventType = eventType, + vehicleId = selectedVehicle!!.vehicleId, + trackId = null + ) + } + isSessionActive = true + sessionStartTime = System.currentTimeMillis() + + // Reset calculator + dragCalculator.resetRealtimeTracking() + currentMetrics = DragMetrics() + speedDataPoints.clear() + chartIndex = 0f + + } else if (isSessionActive) { + // End session + isSessionActive = false + withContext(Dispatchers.IO) { + sessionManager.endSession() + + // Save buffered GPS data + database.rawGPSDataDao().insertAll(dataBuffer) + dataBuffer.clear() } } - }, - contentAlignment = Alignment.Center - ) { - Text( - when { - selectedVehicle == null -> "SELECT VEHICLE FIRST" - isSessionActive -> "STOP SESSION" - else -> "START DRAG" - }, - color = when { - isSessionActive -> TrackProTheme.colors.accentCyan - selectedVehicle == null -> TrackProTheme.colors.textMuted - else -> Color.Black - }, - fontWeight = FontWeight.Black, - fontSize = 13.sp, - letterSpacing = 1.sp - ) - } + } + }, + enabled = selectedVehicle != null, + accent = if (isSessionActive) TrackProTheme.colors.bgElevated else TrackProTheme.colors.accentCyan, + contentColor = if (isSessionActive) TrackProTheme.colors.accentCyan else null, + modifier = Modifier.weight(1f).height(56.dp) + ) } } } @@ -594,62 +518,49 @@ fun DragMetricCard( metric: DragMetricDisplay, modifier: Modifier = Modifier ) { - Box( - modifier = modifier - .background( - if (metric.achieved) TrackProTheme.colors.accentCyan.copy(alpha = 0.1f) - else TrackProTheme.colors.bgCard, - RoundedCornerShape(12.dp) - ) - .border( - 1.5.dp, - if (metric.achieved) TrackProTheme.colors.accentCyan.copy(alpha = 0.6f) - else TrackProTheme.colors.sectorLine, - RoundedCornerShape(12.dp) - ) - .padding(12.dp) + AppCard( + modifier = modifier, + borderColor = if (metric.achieved) TrackProTheme.colors.accentCyan.copy(alpha = 0.6f) else TrackProTheme.colors.sectorLine ) { - Column { + Column( + modifier = Modifier.background( + if (metric.achieved) TrackProTheme.colors.accentCyan.copy(alpha = 0.08f) else Color.Transparent + ) + ) { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( - metric.label, - color = if (metric.achieved) TrackProTheme.colors.accentCyan - else TrackProTheme.colors.textMuted, - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + metric.label.uppercase(), + style = TrackProType.label, + color = if (metric.achieved) TrackProTheme.colors.accentCyan else TrackProTheme.colors.textFaint ) if (metric.achieved) { Box( Modifier - .size(8.dp) + .size(6.dp) .background(TrackProTheme.colors.accentCyan, CircleShape) ) } } - Spacer(Modifier.height(6.dp)) + Spacer(Modifier.height(4.dp)) Row( verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(2.dp) ) { Text( metric.value, - fontSize = 24.sp, - fontWeight = FontWeight.Black, + style = TrackProType.statValue.copy(fontSize = 19.sp), color = if (metric.achieved) TrackProTheme.colors.textPrimary - else TrackProTheme.colors.textMuted.copy(alpha = 0.5f), - letterSpacing = (-0.5).sp + else TrackProTheme.colors.textMuted.copy(alpha = 0.5f) ) Text( metric.unit, - fontSize = 10.sp, - fontWeight = FontWeight.Bold, + style = TrackProType.body.copy(fontSize = 10.sp), color = TrackProTheme.colors.textMuted, - modifier = Modifier.padding(bottom = 3.dp) + modifier = Modifier.padding(bottom = 2.dp) ) } } diff --git a/app/src/main/java/com/example/trackpro/screens/telemetricScreens/TimeAttackScreen.kt b/app/src/main/java/com/example/trackpro/screens/telemetricScreens/TimeAttackScreen.kt index e1454c1..7788764 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 @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,10 +27,8 @@ 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.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView @@ -44,6 +41,14 @@ import com.example.trackpro.dataClasses.LatLonOffset import com.example.trackpro.extrasForUI.TrackProTheme import com.example.trackpro.managerClasses.timeAttackManagers.SectorSplit import com.example.trackpro.managerClasses.timeAttackManagers.TimingMode +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.StatCell +import com.example.trackpro.components.StatCellDivider +import com.example.trackpro.components.StatCellSize +import com.example.trackpro.theme.DataVizColors +import com.example.trackpro.theme.Spacing +import com.example.trackpro.theme.TrackProShapes +import com.example.trackpro.theme.TrackProType import com.example.trackpro.viewModels.TimeAttackViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -221,100 +226,65 @@ fun TimeAttackPortraitLayout( ) { Column(modifier = Modifier.fillMaxSize()) { - // ── Top bar ─────────────────────────────────── - Box( - modifier = Modifier - .fillMaxWidth() - .background(modeColor) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "● $modeLabel MODE", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) + AppTopBar( + title = "$modeLabel Mode", + accent = modeColor, + trailing = { Text( text = if (isConnected) "LIVE" else "NO SIGNAL", - color = if (isConnected) Color.Black else Color.Black.copy(alpha = 0.5f), - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 2.sp + style = TrackProType.label, + color = if (isConnected) TrackProTheme.colors.deltaGood else TrackProTheme.colors.textFaint ) } - } + ) // ── Main timer ──────────────────────────────── Box( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgCard) - .padding(horizontal = 24.dp, vertical = 20.dp) + .padding(horizontal = Spacing.lg, vertical = Spacing.md) ) { Column { Text( - text = "CURRENT $eventName", - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - letterSpacing = 2.sp, - fontWeight = FontWeight.Bold + text = "Current $eventName", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) Text( text = currentTime, - color = deltaColor, - fontSize = 64.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-1).sp, - lineHeight = 68.sp + style = TrackProType.displayNumeric, + color = deltaColor ) Row(verticalAlignment = Alignment.CenterVertically) { Box( modifier = Modifier - .background(deltaColor.copy(alpha = 0.15f), RoundedCornerShape(4.dp)) - .padding(horizontal = 10.dp, vertical = 4.dp) + .background(deltaColor.copy(alpha = 0.15f), TrackProShapes.badge) + .padding(horizontal = Spacing.sm, vertical = 3.dp) ) { Text( text = "Δ ${String.format("%+.3f", delta)}s", - color = deltaColor, - fontSize = 18.sp, - fontWeight = FontWeight.Bold + style = TrackProType.statValue.copy(fontSize = 15.sp), + color = deltaColor ) } if (isLiveDelta) { Spacer(Modifier.width(6.dp)) Text( - text = "● LIVE", - color = deltaColor.copy(alpha = 0.7f), - fontSize = 9.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + text = "LIVE", + style = TrackProType.label, + color = deltaColor.copy(alpha = 0.8f) ) } } } - Column( - modifier = Modifier.align(Alignment.TopEnd), - horizontalAlignment = Alignment.End - ) { - Text( - text = eventName, - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - letterSpacing = 2.sp - ) - Text( - text = "$eventCount", - color = TrackProTheme.colors.textPrimary, - fontSize = 48.sp, - fontWeight = FontWeight.Black - ) - } + StatCell( + label = eventName, + value = "$eventCount", + size = StatCellSize.Large, + horizontalAlignment = Alignment.End, + modifier = Modifier.align(Alignment.TopEnd) + ) } HorizontalDivider(color = TrackProTheme.colors.sectorLine, thickness = 1.dp) @@ -324,13 +294,13 @@ fun TimeAttackPortraitLayout( modifier = Modifier .fillMaxWidth() .background(TrackProTheme.colors.bgElevated) - .padding(horizontal = 24.dp, vertical = 14.dp), + .padding(horizontal = Spacing.lg, vertical = Spacing.md), horizontalArrangement = Arrangement.SpaceBetween ) { - LapTimeCell(label = "BEST", value = bestTime, valueColor = TrackProTheme.colors.deltaGood) - SectorDivider() - LapTimeCell(label = "LAST", value = lastTime, valueColor = TrackProTheme.colors.textPrimary) - SectorDivider() + StatCell(label = "Best", value = bestTime, valueColor = TrackProTheme.colors.deltaGood, size = StatCellSize.Large) + StatCellDivider() + StatCell(label = "Last", value = lastTime, valueColor = TrackProTheme.colors.textPrimary, size = StatCellSize.Large) + StatCellDivider() StintTimerCell(stintStart = stintStart) } @@ -358,19 +328,12 @@ fun TimeAttackPortraitLayout( } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( - "AWAITING GPS SIGNAL", - color = TrackProTheme.colors.textMuted, - fontSize = 12.sp, - letterSpacing = 2.sp + "Awaiting GPS signal", + style = TrackProType.label, + color = TrackProTheme.colors.textFaint ) } } - Text( - text = if (timingMode is TimingMode.Circuit) "◎" else "▶", - color = modeColor.copy(alpha = 0.06f), - fontSize = 180.sp, - modifier = Modifier.align(Alignment.Center) - ) } } } @@ -411,88 +374,68 @@ fun TimeAttackLandscapeLayout( .fillMaxSize() .background(TrackProTheme.colors.bgCard) ) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(modeColor) - .padding(horizontal = 16.dp, vertical = 5.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - "● $modeLabel", - color = Color.Black, - fontSize = 10.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) + AppTopBar( + title = modeLabel, + accent = modeColor, + trailing = { Text( - if (isConnected) "LIVE" else "NO SIGNAL", - color = Color.Black.copy(alpha = if (isConnected) 1f else 0.5f), - fontSize = 10.sp, - fontWeight = FontWeight.Black + text = if (isConnected) "LIVE" else "NO SIGNAL", + style = TrackProType.label, + color = if (isConnected) TrackProTheme.colors.deltaGood else TrackProTheme.colors.textFaint ) } - } + ) - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(Spacing.md)) { Text( - text = "CURRENT $eventName", - color = TrackProTheme.colors.textMuted, - fontSize = 9.sp, - letterSpacing = 2.sp + text = "Current $eventName", + style = TrackProType.label, + color = TrackProTheme.colors.textMuted ) Text( text = currentTime, - color = deltaColor, - fontSize = 48.sp, - fontWeight = FontWeight.Black, - letterSpacing = (-1).sp + style = TrackProType.displayNumeric, + color = deltaColor ) Row(verticalAlignment = Alignment.CenterVertically) { Box( modifier = Modifier - .background(deltaColor.copy(alpha = 0.15f), RoundedCornerShape(4.dp)) - .padding(horizontal = 8.dp, vertical = 3.dp) + .background(deltaColor.copy(alpha = 0.15f), TrackProShapes.badge) + .padding(horizontal = Spacing.sm, vertical = 3.dp) ) { Text( text = "Δ ${String.format("%+.3f", delta)}s", - color = deltaColor, - fontSize = 14.sp, - fontWeight = FontWeight.Bold + style = TrackProType.statValue.copy(fontSize = 13.sp), + color = deltaColor ) } if (isLiveDelta) { Spacer(Modifier.width(4.dp)) Text( - text = "● LIVE", - color = deltaColor.copy(alpha = 0.7f), - fontSize = 8.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + text = "LIVE", + style = TrackProType.label, + color = deltaColor.copy(alpha = 0.8f) ) } } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(Spacing.md)) HorizontalDivider(color = TrackProTheme.colors.sectorLine) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(Spacing.md)) - Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) { - LapTimeCell(label = "BEST", value = bestTime, valueColor = TrackProTheme.colors.deltaGood) - LapTimeCell(label = "LAST", value = lastTime, valueColor = TrackProTheme.colors.textPrimary) + Row(horizontalArrangement = Arrangement.spacedBy(Spacing.xl)) { + StatCell(label = "Best", value = bestTime, valueColor = TrackProTheme.colors.deltaGood, size = StatCellSize.Large) + StatCell(label = "Last", value = lastTime, valueColor = TrackProTheme.colors.textPrimary, size = StatCellSize.Large) } if (lapSplits.isNotEmpty()) { - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(Spacing.sm)) SectorSplitsRow(splits = lapSplits) } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(Spacing.md)) HorizontalDivider(color = TrackProTheme.colors.sectorLine) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(Spacing.md)) Row( modifier = Modifier.fillMaxWidth(), @@ -500,20 +443,13 @@ fun TimeAttackLandscapeLayout( verticalAlignment = Alignment.CenterVertically ) { StintTimerCell(stintStart = stintStart) - Column(horizontalAlignment = Alignment.End) { - Text( - eventName, - color = TrackProTheme.colors.textMuted, - fontSize = 9.sp, - letterSpacing = 2.sp - ) - Text( - "$eventCount", - color = modeColor, - fontSize = 38.sp, - fontWeight = FontWeight.Black - ) - } + StatCell( + label = eventName, + value = "$eventCount", + valueColor = modeColor, + size = StatCellSize.Large, + horizontalAlignment = Alignment.End + ) } } } @@ -535,10 +471,9 @@ fun TimeAttackLandscapeLayout( } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( - "AWAITING GPS", - color = TrackProTheme.colors.textMuted, - fontSize = 11.sp, - letterSpacing = 2.sp + "Awaiting GPS", + style = TrackProType.label, + color = TrackProTheme.colors.textFaint ) } } @@ -548,25 +483,6 @@ fun TimeAttackLandscapeLayout( // ── Shared sub-components ────────────────────────────────── -@Composable -private fun LapTimeCell(label: String, value: String, valueColor: Color) { - Column { - Text( - text = label, - color = TrackProTheme.colors.textMuted, - fontSize = 9.sp, - letterSpacing = 2.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = value, - color = valueColor, - fontSize = 20.sp, - fontWeight = FontWeight.Bold - ) - } -} - @Composable private fun StintTimerCell(stintStart: Long) { var stintTime by remember { mutableStateOf("00:00:00") } @@ -580,21 +496,7 @@ private fun StintTimerCell(stintStart: Long) { stintTime = String.format("%02d:%02d:%02d", h, m, s) } } - Column { - Text( - text = "STINT", - color = TrackProTheme.colors.textMuted, - fontSize = 9.sp, - letterSpacing = 2.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = stintTime, - color = TrackProTheme.colors.textPrimary, - fontSize = 20.sp, - fontWeight = FontWeight.Bold - ) - } + StatCell(label = "Stint", value = stintTime, size = StatCellSize.Large) } @Composable @@ -602,8 +504,8 @@ private fun SectorSplitsRow(splits: List) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 4.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(18.dp) + .padding(horizontal = 4.dp, vertical = Spacing.sm), + horizontalArrangement = Arrangement.spacedBy(Spacing.lg) ) { splits.forEach { split -> val deltaColor = when { @@ -614,41 +516,27 @@ private fun SectorSplitsRow(splits: List) { Column { Text( "S${split.sectorIndex + 1}", - color = TrackProTheme.colors.textMuted, - fontSize = 9.sp, - letterSpacing = 1.sp, - fontWeight = FontWeight.Bold + style = TrackProType.label.copy(fontSize = 9.sp, letterSpacing = 0.5.sp), + color = TrackProTheme.colors.textFaint ) Text( String.format("%.2fs", split.splitMs / 1000.0), - color = deltaColor, - fontSize = 14.sp, - fontWeight = FontWeight.Bold + style = TrackProType.statValue.copy(fontSize = 13.sp), + color = deltaColor ) if (split.deltaMs != null) { val deltaSeconds = split.deltaMs / 1000.0 val sign = if (deltaSeconds > 0) "+" else "" Text( "$sign${String.format("%.2f", deltaSeconds)}", - color = deltaColor, - fontSize = 9.sp, - fontWeight = FontWeight.Bold + style = TrackProType.body.copy(fontSize = 9.sp), + color = deltaColor ) } } } } } - -@Composable -private fun SectorDivider() { - Box( - modifier = Modifier - .width(1.dp) - .height(40.dp) - .background(TrackProTheme.colors.sectorLine) - ) -} // ── Reusable sub-components ──────────────────────────────── @Composable fun MapLibreTrackView( @@ -710,9 +598,9 @@ fun MapLibreTrackView( style.addLayer( CircleLayer("driver-layer", "driver-src").apply { setProperties( - PropertyFactory.circleColor("#FFFFFF"), + PropertyFactory.circleColor(DataVizColors.boundaryLine), PropertyFactory.circleRadius(6f), - PropertyFactory.circleStrokeColor("#E8001C"), + PropertyFactory.circleStrokeColor(DataVizColors.trackLine), PropertyFactory.circleStrokeWidth(2f) ) } @@ -786,7 +674,7 @@ private fun drawTrackOnStyle( style.addLayer( LineLayer("track-layer", "track-src").apply { setProperties( - PropertyFactory.lineColor("#E8001C"), + PropertyFactory.lineColor(DataVizColors.trackLine), PropertyFactory.lineWidth(4f), PropertyFactory.lineCap(Property.LINE_CAP_ROUND), PropertyFactory.lineJoin(Property.LINE_JOIN_ROUND) @@ -826,7 +714,7 @@ private fun drawBoundaries(style: Style, path: List) { style.addSource(GeoJsonSource(id, geojson)) style.addLayer(LineLayer("$id-layer", id).apply { setProperties( - PropertyFactory.lineColor("#FFFFFF"), + PropertyFactory.lineColor(DataVizColors.boundaryLine), PropertyFactory.lineWidth(1.5f), PropertyFactory.lineOpacity(0.3f), PropertyFactory.lineDasharray(arrayOf(2f, 2f)) @@ -850,10 +738,10 @@ private fun drawMarkers(style: Style, timingLines: List) { style.addSource(GeoJsonSource(id, geojson)) style.addLayer(CircleLayer("$id-layer", id).apply { setProperties( - PropertyFactory.circleColor("#FFFFFF"), + PropertyFactory.circleColor(DataVizColors.boundaryLine), PropertyFactory.circleRadius(6f), PropertyFactory.circleStrokeWidth(2f), - PropertyFactory.circleStrokeColor("#E8001C") + PropertyFactory.circleStrokeColor(DataVizColors.trackLine) ) }) } 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 0c19880..88b6450 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 @@ -2,7 +2,6 @@ package com.example.trackpro.screens.vehicleScreens import android.widget.Toast import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -11,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Business @@ -23,9 +21,6 @@ import androidx.compose.material.icons.filled.LocalGasStation import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Speed import androidx.compose.material.icons.filled.Timer -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -35,17 +30,19 @@ import androidx.compose.runtime.rememberCoroutineScope 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.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.example.trackpro.TrackProApp import com.example.trackpro.dataClasses.VehicleInformationData import com.example.trackpro.managerClasses.ESPDatabase +import com.example.trackpro.extrasForUI.AppDropdownField import com.example.trackpro.extrasForUI.CustomTextField -import com.example.trackpro.extrasForUI.DropdownMenuField import com.example.trackpro.extrasForUI.TrackProTheme +import com.example.trackpro.components.AppCard +import com.example.trackpro.components.AppTopBar +import com.example.trackpro.components.PrimaryButton +import com.example.trackpro.components.SectionLabel +import com.example.trackpro.theme.Spacing import com.example.trackpro.managerClasses.JsonReader.loadJsonOptions import com.example.trackpro.managerClasses.utilities.UnitFormatter import kotlinx.coroutines.launch @@ -86,124 +83,82 @@ fun CarCreationScreen( ) { Column(modifier = Modifier.fillMaxSize()) { - // ── Top bar ─────────────────────────────────── - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.accentAmber) - .padding(horizontal = 20.dp, vertical = 6.dp) - ) { - Text( - text = "● VEHICLE SETUP", - color = Color.Black, - fontSize = 11.sp, - fontWeight = FontWeight.Black, - letterSpacing = 3.sp - ) - } + AppTopBar(title = "Vehicle Setup", accent = TrackProTheme.colors.accentAmber) Column( modifier = Modifier .verticalScroll(scrollState) .fillMaxWidth() - .padding(16.dp), + .padding(Spacing.md), horizontalAlignment = Alignment.CenterHorizontally ) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(TrackProTheme.colors.bgCard, RoundedCornerShape(12.dp)) - .border(1.dp, TrackProTheme.colors.sectorLine, RoundedCornerShape(12.dp)) - ) { - Column(modifier = Modifier.padding(20.dp)) { - - SectionTitle("Basic Info (Required)") - 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 } - - SectionTitle("Performance") - CustomTextField("Horsepower", horsepower, true, Icons.Default.FlashOn) { horsepower = it } - CustomTextField("Torque (Nm)", torque, true, Icons.Default.Settings) { torque = it } - CustomTextField("Weight (kg)", weight, true, Icons.Default.FitnessCenter) { weight = it } - CustomTextField("Top Speed (${UnitFormatter.speedUnitLabel(useMetric)})", topSpeed, true, Icons.Default.Speed) { topSpeed = it } - CustomTextField( - if (useMetric) "0-100 KM/H (s)" else "0-60 MPH (s)", - acceleration, true, Icons.Default.Timer - ) { acceleration = it } - CustomTextField("Fuel Capacity (L)", fuelCapacity, true, Icons.Default.LocalGasStation) { fuelCapacity = it } - - SectionTitle("Configuration") - DropdownMenuField("Engine Type", jsonOptions.engineTypes, selectedEngineType) { selectedEngineType = it } - DropdownMenuField("Drivetrain", jsonOptions.drivetrains, selectedDrivetrain) { selectedDrivetrain = it } - DropdownMenuField("Fuel Type", jsonOptions.fuelTypes, selectedFuelType) { selectedFuelType = it } - DropdownMenuField("Tire Type", jsonOptions.tireTypes, selectedTireType) { selectedTireType = it } - DropdownMenuField("Transmission", jsonOptions.transmissions, selectedTransmission) { selectedTransmission = it } - DropdownMenuField("Suspension", jsonOptions.suspensionTypes, selectedSuspensionType) { selectedSuspensionType = it } - - Spacer(modifier = Modifier.height(20.dp)) - - Button( - onClick = { - if (manufacturer.isBlank() || model.isBlank() || year.isBlank()) { - Toast.makeText(context, "Fill in required fields.", Toast.LENGTH_SHORT).show() - return@Button - } - - val vehicle = VehicleInformationData( - manufacturer = manufacturer, - model = model, - year = year.toIntOrNull() ?: 0, - engineType = selectedEngineType, - horsepower = horsepower.toIntOrNull() ?: 0, - torque = torque.toIntOrNull(), - weight = weight.toDoubleOrNull() ?: 0.0, - // Stored canonically in km/h regardless of the unit the - // user entered it in, matching every other speed value. - topSpeed = topSpeed.toDoubleOrNull()?.let { UnitFormatter.convertSpeedToKmh(it, useMetric) }, - acceleration = acceleration.toDoubleOrNull(), - drivetrain = selectedDrivetrain, - fuelType = selectedFuelType, - tireType = selectedTireType, - fuelCapacity = fuelCapacity.toDoubleOrNull(), - transmission = selectedTransmission, - suspensionType = selectedSuspensionType - ) - - coroutineScope.launch { - database.vehicleInformationDAO().insertVehicle(vehicle) - } - - Toast.makeText(context, "Vehicle saved successfully.", Toast.LENGTH_SHORT).show() - }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors(containerColor = TrackProTheme.colors.deltaGood), - shape = RoundedCornerShape(8.dp) - ) { - Text( - "SAVE VEHICLE", - color = Color.Black, - fontSize = 13.sp, - fontWeight = FontWeight.Black, - letterSpacing = 1.sp + 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 } + + 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( + if (useMetric) "0-100 KM/H (s)" else "0-60 MPH (s)", + acceleration, true, Icons.Default.Timer, accent = TrackProTheme.colors.accentAmber + ) { acceleration = it } + CustomTextField("Fuel Capacity (L)", fuelCapacity, true, Icons.Default.LocalGasStation, accent = TrackProTheme.colors.accentAmber) { 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) + + Spacer(modifier = Modifier.height(Spacing.md)) + + PrimaryButton( + text = "Save Vehicle", + onClick = { + if (manufacturer.isBlank() || model.isBlank() || year.isBlank()) { + Toast.makeText(context, "Fill in required fields.", Toast.LENGTH_SHORT).show() + return@PrimaryButton + } + + val vehicle = VehicleInformationData( + manufacturer = manufacturer, + model = model, + year = year.toIntOrNull() ?: 0, + engineType = selectedEngineType, + horsepower = horsepower.toIntOrNull() ?: 0, + torque = torque.toIntOrNull(), + weight = weight.toDoubleOrNull() ?: 0.0, + // Stored canonically in km/h regardless of the unit the + // user entered it in, matching every other speed value. + topSpeed = topSpeed.toDoubleOrNull()?.let { UnitFormatter.convertSpeedToKmh(it, useMetric) }, + acceleration = acceleration.toDoubleOrNull(), + drivetrain = selectedDrivetrain, + fuelType = selectedFuelType, + tireType = selectedTireType, + fuelCapacity = fuelCapacity.toDoubleOrNull(), + transmission = selectedTransmission, + suspensionType = selectedSuspensionType ) - } - } + + coroutineScope.launch { + database.vehicleInformationDAO().insertVehicle(vehicle) + } + + Toast.makeText(context, "Vehicle saved successfully.", Toast.LENGTH_SHORT).show() + }, + accent = TrackProTheme.colors.deltaGood, + modifier = Modifier.fillMaxWidth() + ) } } } } } - - -@Composable -fun SectionTitle(title: String) { - Text( - text = title.uppercase(), - color = TrackProTheme.colors.textMuted, - fontSize = 10.sp, - letterSpacing = 2.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(vertical = 8.dp) - ) -} 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 a2c5db8..939bc8a 100644 --- a/app/src/main/java/com/example/trackpro/theme/TrackProColors.kt +++ b/app/src/main/java/com/example/trackpro/theme/TrackProColors.kt @@ -11,36 +11,47 @@ data class TrackProColorScheme( val accentAmber: Color, val textPrimary: Color, val textMuted: Color, + val textFaint: Color, val deltaGood: Color, val deltaBad: Color, - val sectorLine: Color + val sectorLine: Color, + val danger: Color ) - +/** + * 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. + */ val DarkTrackProColors = TrackProColorScheme( - bgDeep = Color(0xFF0A0C11), - bgCard = Color(0xFF151924), - bgElevated = Color(0xFF212739), - accentCyan = Color(0xFF22D3EE), - accentBlue = Color(0xFF818CF8), - accentAmber = Color(0xFFFBBF24), - textPrimary = Color(0xFFF3F5F9), - textMuted = Color(0xFF9AA5B8), - deltaGood = Color(0xFF4ADE80), - deltaBad = Color(0xFFF87171), - sectorLine = Color(0xFF3A4358) + 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) ) val LightTrackProColors = TrackProColorScheme( - bgDeep = Color(0xFFEEF1F6), + bgDeep = Color(0xFFF0F2F5), bgCard = Color(0xFFFFFFFF), - bgElevated = Color(0xFFE2E8F1), - accentCyan = Color(0xFF0891B2), - accentBlue = Color(0xFF6366F1), - accentAmber = Color(0xFFD97706), - textPrimary = Color(0xFF0F172A), - textMuted = Color(0xFF64748B), - deltaGood = Color(0xFF16A34A), - deltaBad = Color(0xFFDC2626), - sectorLine = Color(0xFFD8DEE9) + 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