feat(android): UI redesign — light web-matching theme, dark top bar + bottom nav (Phase 28.7)
This commit is contained in:
@@ -62,8 +62,6 @@ fun DashboardScreen(
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
com.yarisobd.app.ui.components.YarisHeader()
|
||||
|
||||
// Connection status bar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
|
||||
@@ -69,7 +69,6 @@ fun DashcamScreen(
|
||||
|
||||
// Auto-refresh on first load
|
||||
LaunchedEffect(Unit) { onRefresh() }
|
||||
com.yarisobd.app.ui.components.YarisHeader()
|
||||
|
||||
// USB Dashcam section
|
||||
Card(
|
||||
|
||||
@@ -75,8 +75,6 @@ fun DownloadsScreen(
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
com.yarisobd.app.ui.components.YarisHeader()
|
||||
|
||||
// Header card
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -32,9 +33,11 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.yarisobd.app.data.remote.dto.TripSummary
|
||||
import com.yarisobd.app.ui.components.YarisHeader
|
||||
import com.yarisobd.app.ui.theme.YarisOBDTheme
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@@ -98,6 +101,14 @@ fun YarisMainScreen(client: OkHttpClient) {
|
||||
var selectedTrip by remember { mutableStateOf<TripSummary?>(null) }
|
||||
var selectedPortalTrip by remember { mutableStateOf<TripSummary?>(null) }
|
||||
|
||||
val navItemColors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = Color.White,
|
||||
selectedTextColor = Color.White,
|
||||
indicatorColor = Color(0x26FFFFFF),
|
||||
unselectedIconColor = Color(0x8CFFFFFF),
|
||||
unselectedTextColor = Color(0x8CFFFFFF),
|
||||
)
|
||||
|
||||
// Show download/delete events as snackbars
|
||||
LaunchedEffect(Unit) {
|
||||
dashcamVm.downloadEvents.collect { event ->
|
||||
@@ -130,44 +141,51 @@ fun YarisMainScreen(client: OkHttpClient) {
|
||||
)
|
||||
} else {
|
||||
Scaffold(
|
||||
topBar = { YarisHeader() },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
NavigationBar(containerColor = Color(0xFF1C1C1E)) {
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == 0,
|
||||
onClick = { selectedTab = 0 },
|
||||
icon = { Icon(Icons.Filled.Dashboard, contentDescription = "Dashboard") },
|
||||
label = { Text("Dashboard") }
|
||||
label = { Text("Dashboard") },
|
||||
colors = navItemColors,
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == 1,
|
||||
onClick = { selectedTab = 1 },
|
||||
icon = { Icon(Icons.Filled.Videocam, contentDescription = "Dashcam") },
|
||||
label = { Text("Dashcam") }
|
||||
label = { Text("Dashcam") },
|
||||
colors = navItemColors,
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == 2,
|
||||
onClick = { selectedTab = 2 },
|
||||
icon = { Icon(Icons.Filled.FileDownload, contentDescription = "Downloads") },
|
||||
label = { Text("Downloads") }
|
||||
label = { Text("Downloads") },
|
||||
colors = navItemColors,
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == 3,
|
||||
onClick = { selectedTab = 3 },
|
||||
icon = { Icon(Icons.Filled.DirectionsCar, contentDescription = "Trips") },
|
||||
label = { Text("Trips") }
|
||||
label = { Text("Trips") },
|
||||
colors = navItemColors,
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == 4,
|
||||
onClick = { selectedTab = 4 },
|
||||
icon = { Icon(Icons.Filled.Settings, contentDescription = "Settings") },
|
||||
label = { Text("Settings") }
|
||||
label = { Text("Settings") },
|
||||
colors = navItemColors,
|
||||
)
|
||||
NavigationBarItem(
|
||||
selected = selectedTab == 5,
|
||||
onClick = { selectedTab = 5 },
|
||||
icon = { Icon(Icons.Filled.Cloud, contentDescription = "Server") },
|
||||
label = { Text("Server") }
|
||||
label = { Text("Server") },
|
||||
colors = navItemColors,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,6 @@ fun PortalScreen(
|
||||
var password by remember { mutableStateOf(CredentialStore.getPortalPassword(context)) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
com.yarisobd.app.ui.components.YarisHeader()
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@@ -49,7 +49,6 @@ fun SettingsScreen(client: OkHttpClient) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
||||
) {
|
||||
com.yarisobd.app.ui.components.YarisHeader()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Server credentials
|
||||
|
||||
@@ -47,8 +47,6 @@ fun TripListScreen(
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
com.yarisobd.app.ui.components.YarisHeader()
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@@ -11,9 +11,10 @@ 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.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -32,6 +33,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -50,6 +52,7 @@ import com.yarisobd.app.data.remote.dto.RoutePoint
|
||||
import com.yarisobd.app.data.remote.dto.TripMeta
|
||||
import com.yarisobd.app.data.remote.dto.TripRoute
|
||||
import com.yarisobd.app.data.remote.dto.TripSummary
|
||||
import com.yarisobd.app.ui.theme.YarisDark
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.Locale
|
||||
|
||||
@@ -90,26 +93,29 @@ fun TripReplayScreen(
|
||||
if (currentIndex >= points.size - 1) playing = false
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().safeDrawingPadding()) {
|
||||
// Top bar: back + title
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
"Trip #${trip.id}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
dateText(trip.start_datetime),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Top bar: back + title (dark, matching the global header)
|
||||
Surface(color = YarisDark) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().statusBarsPadding().padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = Color.White)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
"Trip #${trip.id}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
dateText(trip.start_datetime),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +150,7 @@ fun TripReplayScreen(
|
||||
|
||||
// Stats + scrubber
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp),
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).navigationBarsPadding().padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
TripStatsHeader(route?.trip ?: TripMeta())
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
package com.yarisobd.app.ui.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
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 com.yarisobd.app.ui.theme.YarisDark
|
||||
import com.yarisobd.app.ui.theme.YarisRed
|
||||
|
||||
/** Shared branding header matching web UI style */
|
||||
/** Global dark top bar matching the web UI's mobile topbar + YARIS wordmark. */
|
||||
@Composable
|
||||
fun YarisHeader() {
|
||||
Text(
|
||||
"YARIS-OBD",
|
||||
style = MaterialTheme.typography.headlineMedium.copy(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontStyle = FontStyle.Italic,
|
||||
letterSpacing = 1.5.sp,
|
||||
),
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||
)
|
||||
Spacer(Modifier.height(1.dp))
|
||||
// Red accent underline — 48dp wide
|
||||
Canvas(
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
.width(48.dp)
|
||||
.height(3.dp)
|
||||
) {
|
||||
drawRect(color = YarisRed)
|
||||
Surface(color = YarisDark) {
|
||||
Row(Modifier.fillMaxWidth().statusBarsPadding()) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.drawBehind {
|
||||
drawRect(
|
||||
color = YarisRed,
|
||||
topLeft = Offset(0f, size.height - 4.dp.toPx()),
|
||||
size = Size(size.width, 3.dp.toPx()),
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"YARIS-OBD",
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontStyle = FontStyle.Italic,
|
||||
letterSpacing = 1.2.sp,
|
||||
),
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
"phase 28 — android companion",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,46 +16,56 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
// Brand colors — matching web UI: #1c1c1e sidebar, #e63946 red, green primary
|
||||
private val YarisGreen = Color(0xFF4CAF50)
|
||||
private val YarisGreenDark = Color(0xFF388E3C)
|
||||
val YarisRed = Color(0xFFE63946)
|
||||
private val YarisBackground = Color(0xFF1C1C1E)
|
||||
private val YarisSurface = Color(0xFF2A2A2C)
|
||||
private val YarisSurfaceVariant = Color(0xFF3A3A3C)
|
||||
// Brand colors — matching static/yaris.css (mobile web UI)
|
||||
val YarisRed = Color(0xFFE63946) // --accent
|
||||
val YarisDark = Color(0xFF1C1C1E) // --sidebar-bg (top bar + bottom nav)
|
||||
val YarisBlue = Color(0xFF4E73DF) // --primary
|
||||
val YarisBody = Color(0xFFF4F6FB) // body background
|
||||
val YarisMuted = Color(0xFF6C757D) // text-muted
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = YarisGreen,
|
||||
onPrimary = Color.Black,
|
||||
primaryContainer = YarisGreenDark,
|
||||
secondary = Color(0xFF81C784),
|
||||
background = YarisBackground,
|
||||
surface = YarisSurface,
|
||||
surfaceContainerHigh = YarisSurfaceVariant,
|
||||
onBackground = Color(0xFFE0E0E0),
|
||||
onSurface = Color(0xFFE0E0E0),
|
||||
onSurfaceVariant = Color(0xFFAAAAAA),
|
||||
error = Color(0xFFEF5350),
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = YarisBlue,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Color(0xFFD6E0FB),
|
||||
onPrimaryContainer = Color(0xFF1B2A5A),
|
||||
secondary = YarisBlue,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = Color(0xFFE8EDFB),
|
||||
onSecondaryContainer = Color(0xFF1B2A5A),
|
||||
tertiary = Color(0xFF36B9CC),
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = Color(0xFFD7F2F8),
|
||||
onTertiaryContainer = Color(0xFF0A3A42),
|
||||
background = YarisBody,
|
||||
onBackground = Color(0xFF212529),
|
||||
surface = Color.White,
|
||||
onSurface = Color(0xFF212529),
|
||||
surfaceVariant = Color(0xFFEDF0F5),
|
||||
onSurfaceVariant = YarisMuted,
|
||||
surfaceContainerHigh = Color.White,
|
||||
surfaceContainer = Color.White,
|
||||
error = YarisRed,
|
||||
onError = Color.White,
|
||||
errorContainer = Color(0xFFFBE3E5),
|
||||
onErrorContainer = Color(0xFF5A1016),
|
||||
outline = Color(0xFFCED4DA),
|
||||
)
|
||||
|
||||
// Light scheme exists for completeness but the app defaults to dark
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = YarisGreenDark,
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = YarisBlue,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Color(0xFFC8E6C9),
|
||||
secondary = Color(0xFF43A047),
|
||||
background = Color(0xFFFAFAFA),
|
||||
surface = Color.White,
|
||||
surfaceContainerHigh = Color(0xFFF5F5F5),
|
||||
onBackground = Color(0xFF212121),
|
||||
onSurface = Color(0xFF212121),
|
||||
onSurfaceVariant = Color(0xFF757575),
|
||||
error = Color(0xFFD32F2F),
|
||||
background = YarisDark,
|
||||
onBackground = Color(0xFFE0E0E0),
|
||||
surface = YarisDark,
|
||||
onSurface = Color(0xFFE0E0E0),
|
||||
surfaceContainerHigh = Color(0xFF2A2A2C),
|
||||
onSurfaceVariant = Color(0xFFAAAAAA),
|
||||
error = YarisRed,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun YarisOBDTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
darkTheme: Boolean = false, // the web UI is light — match it by default
|
||||
dynamicColor: Boolean = false, // Off by default — use our brand colors
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
@@ -72,8 +82,9 @@ fun YarisOBDTheme(
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colorScheme.background.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||
window.statusBarColor = YarisDark.toArgb()
|
||||
// Dark top bar sits under the status bar, so status icons stay light.
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user