feat(android): UI redesign — light web-matching theme, dark top bar + bottom nav (Phase 28.7)
This commit is contained in:
@@ -2021,7 +2021,7 @@ tracked in FEATURES.md.
|
|||||||
2. **FEAT-02** — QR / claim-code device pairing flow (medium)
|
2. **FEAT-02** — QR / claim-code device pairing flow (medium)
|
||||||
3. **FEAT-04** — Replace SIM868 2G GSM with 4G LTE modem (large)
|
3. **FEAT-04** — Replace SIM868 2G GSM with 4G LTE modem (large)
|
||||||
|
|
||||||
### Phase 28: Native Android companion app — 🚧 IN PROGRESS (28.1 ✅, 28.2 ✅, 28.3 ✅, 28.4 ✅, 28.5 ✅, 28.6 ✅, 28.7 pending)
|
### Phase 28: Native Android companion app — 🚧 IN PROGRESS (28.1 ✅, 28.2 ✅, 28.3 ✅, 28.4 ✅, 28.5 ✅, 28.6 ✅, 28.7 ✅, 28.8 pending)
|
||||||
**Renumbered from former Phase 26 on 2026-08-10 to make room for the road-test bug-fix phase.
|
**Renumbered from former Phase 26 on 2026-08-10 to make room for the road-test bug-fix phase.
|
||||||
Renumbered to Phase 28 on 2026-08-11 to make room for Phase 27 feature additions. Planning
|
Renumbered to Phase 28 on 2026-08-11 to make room for Phase 27 feature additions. Planning
|
||||||
completed 2026-08-11. Detailed implementation plan in [android/plan.md](android/plan.md).**
|
completed 2026-08-11. Detailed implementation plan in [android/plan.md](android/plan.md).**
|
||||||
@@ -2220,7 +2220,16 @@ The app reuses the existing Pi REST API surface with no changes:
|
|||||||
- Deferred: dashcam video streaming/player, session-cookie persistence across launches (currently
|
- Deferred: dashcam video streaming/player, session-cookie persistence across launches (currently
|
||||||
re-logs-in each launch), and a server-side tile source for the replay map when off the hotspot.
|
re-logs-in each launch), and a server-side tile source for the replay map when off the hotspot.
|
||||||
|
|
||||||
**28.7 — Polish + distribution**
|
**28.7 — UI redesign: match the mobile web UI ✅ DONE (2026-08-15)**
|
||||||
|
- Light theme matching `static/yaris.css` (content `#f4f6fb`, white cards, primary
|
||||||
|
`#4e73df`, accent red `#e63946`) — the app now defaults to light like the web.
|
||||||
|
- Global dark top bar (`YarisHeader`) with the white italic "YARIS-OBD" wordmark +
|
||||||
|
red underline, and a dark bottom nav (`#1c1c1e`) mirroring the web's
|
||||||
|
`.mobile-bottom-nav`. Per-screen headers removed; the replay screen got the same
|
||||||
|
dark top bar.
|
||||||
|
- Follow-up polish deferred: card shadows, blue `btn-primary` buttons, page titles.
|
||||||
|
|
||||||
|
**28.8 — Polish + distribution**
|
||||||
- App icon, splash screen, dark theme polish
|
- App icon, splash screen, dark theme polish
|
||||||
- Error handling: Pi unreachable, server unreachable, no cellular data, storage full
|
- Error handling: Pi unreachable, server unreachable, no cellular data, storage full
|
||||||
- APK signing + release build
|
- APK signing + release build
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
com.yarisobd.app.ui.components.YarisHeader()
|
|
||||||
|
|
||||||
// Connection status bar
|
// Connection status bar
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp),
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ fun DashcamScreen(
|
|||||||
|
|
||||||
// Auto-refresh on first load
|
// Auto-refresh on first load
|
||||||
LaunchedEffect(Unit) { onRefresh() }
|
LaunchedEffect(Unit) { onRefresh() }
|
||||||
com.yarisobd.app.ui.components.YarisHeader()
|
|
||||||
|
|
||||||
// USB Dashcam section
|
// USB Dashcam section
|
||||||
Card(
|
Card(
|
||||||
|
|||||||
@@ -75,8 +75,6 @@ fun DownloadsScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
com.yarisobd.app.ui.components.YarisHeader()
|
|
||||||
|
|
||||||
// Header card
|
// Header card
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@@ -32,9 +33,11 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.yarisobd.app.data.remote.dto.TripSummary
|
import com.yarisobd.app.data.remote.dto.TripSummary
|
||||||
|
import com.yarisobd.app.ui.components.YarisHeader
|
||||||
import com.yarisobd.app.ui.theme.YarisOBDTheme
|
import com.yarisobd.app.ui.theme.YarisOBDTheme
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
@@ -98,6 +101,14 @@ fun YarisMainScreen(client: OkHttpClient) {
|
|||||||
var selectedTrip by remember { mutableStateOf<TripSummary?>(null) }
|
var selectedTrip by remember { mutableStateOf<TripSummary?>(null) }
|
||||||
var selectedPortalTrip 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
|
// Show download/delete events as snackbars
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
dashcamVm.downloadEvents.collect { event ->
|
dashcamVm.downloadEvents.collect { event ->
|
||||||
@@ -130,44 +141,51 @@ fun YarisMainScreen(client: OkHttpClient) {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
topBar = { YarisHeader() },
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar(containerColor = Color(0xFF1C1C1E)) {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == 0,
|
selected = selectedTab == 0,
|
||||||
onClick = { selectedTab = 0 },
|
onClick = { selectedTab = 0 },
|
||||||
icon = { Icon(Icons.Filled.Dashboard, contentDescription = "Dashboard") },
|
icon = { Icon(Icons.Filled.Dashboard, contentDescription = "Dashboard") },
|
||||||
label = { Text("Dashboard") }
|
label = { Text("Dashboard") },
|
||||||
|
colors = navItemColors,
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == 1,
|
selected = selectedTab == 1,
|
||||||
onClick = { selectedTab = 1 },
|
onClick = { selectedTab = 1 },
|
||||||
icon = { Icon(Icons.Filled.Videocam, contentDescription = "Dashcam") },
|
icon = { Icon(Icons.Filled.Videocam, contentDescription = "Dashcam") },
|
||||||
label = { Text("Dashcam") }
|
label = { Text("Dashcam") },
|
||||||
|
colors = navItemColors,
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == 2,
|
selected = selectedTab == 2,
|
||||||
onClick = { selectedTab = 2 },
|
onClick = { selectedTab = 2 },
|
||||||
icon = { Icon(Icons.Filled.FileDownload, contentDescription = "Downloads") },
|
icon = { Icon(Icons.Filled.FileDownload, contentDescription = "Downloads") },
|
||||||
label = { Text("Downloads") }
|
label = { Text("Downloads") },
|
||||||
|
colors = navItemColors,
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == 3,
|
selected = selectedTab == 3,
|
||||||
onClick = { selectedTab = 3 },
|
onClick = { selectedTab = 3 },
|
||||||
icon = { Icon(Icons.Filled.DirectionsCar, contentDescription = "Trips") },
|
icon = { Icon(Icons.Filled.DirectionsCar, contentDescription = "Trips") },
|
||||||
label = { Text("Trips") }
|
label = { Text("Trips") },
|
||||||
|
colors = navItemColors,
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == 4,
|
selected = selectedTab == 4,
|
||||||
onClick = { selectedTab = 4 },
|
onClick = { selectedTab = 4 },
|
||||||
icon = { Icon(Icons.Filled.Settings, contentDescription = "Settings") },
|
icon = { Icon(Icons.Filled.Settings, contentDescription = "Settings") },
|
||||||
label = { Text("Settings") }
|
label = { Text("Settings") },
|
||||||
|
colors = navItemColors,
|
||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedTab == 5,
|
selected = selectedTab == 5,
|
||||||
onClick = { selectedTab = 5 },
|
onClick = { selectedTab = 5 },
|
||||||
icon = { Icon(Icons.Filled.Cloud, contentDescription = "Server") },
|
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)) }
|
var password by remember { mutableStateOf(CredentialStore.getPortalPassword(context)) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
com.yarisobd.app.ui.components.YarisHeader()
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ fun SettingsScreen(client: OkHttpClient) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
com.yarisobd.app.ui.components.YarisHeader()
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
// Server credentials
|
// Server credentials
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ fun TripListScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
com.yarisobd.app.ui.components.YarisHeader()
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
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.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -32,6 +33,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.TripMeta
|
||||||
import com.yarisobd.app.data.remote.dto.TripRoute
|
import com.yarisobd.app.data.remote.dto.TripRoute
|
||||||
import com.yarisobd.app.data.remote.dto.TripSummary
|
import com.yarisobd.app.data.remote.dto.TripSummary
|
||||||
|
import com.yarisobd.app.ui.theme.YarisDark
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@@ -90,26 +93,29 @@ fun TripReplayScreen(
|
|||||||
if (currentIndex >= points.size - 1) playing = false
|
if (currentIndex >= points.size - 1) playing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize().safeDrawingPadding()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Top bar: back + title
|
// Top bar: back + title (dark, matching the global header)
|
||||||
Row(
|
Surface(color = YarisDark) {
|
||||||
Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
Modifier.fillMaxWidth().statusBarsPadding().padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
) {
|
verticalAlignment = Alignment.CenterVertically
|
||||||
IconButton(onClick = onBack) {
|
) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
IconButton(onClick = onBack) {
|
||||||
}
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = Color.White)
|
||||||
Column {
|
}
|
||||||
Text(
|
Column {
|
||||||
"Trip #${trip.id}",
|
Text(
|
||||||
style = MaterialTheme.typography.titleMedium,
|
"Trip #${trip.id}",
|
||||||
fontWeight = FontWeight.SemiBold
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
fontWeight = FontWeight.SemiBold,
|
||||||
Text(
|
color = Color.White
|
||||||
dateText(trip.start_datetime),
|
)
|
||||||
style = MaterialTheme.typography.bodySmall,
|
Text(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
dateText(trip.start_datetime),
|
||||||
)
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.White.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +150,7 @@ fun TripReplayScreen(
|
|||||||
|
|
||||||
// Stats + scrubber
|
// Stats + scrubber
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp),
|
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).navigationBarsPadding().padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
TripStatsHeader(route?.trip ?: TripMeta())
|
TripStatsHeader(route?.trip ?: TripMeta())
|
||||||
|
|||||||
@@ -1,52 +1,53 @@
|
|||||||
package com.yarisobd.app.ui.components
|
package com.yarisobd.app.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
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.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.yarisobd.app.ui.theme.YarisDark
|
||||||
import com.yarisobd.app.ui.theme.YarisRed
|
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
|
@Composable
|
||||||
fun YarisHeader() {
|
fun YarisHeader() {
|
||||||
Text(
|
Surface(color = YarisDark) {
|
||||||
"YARIS-OBD",
|
Row(Modifier.fillMaxWidth().statusBarsPadding()) {
|
||||||
style = MaterialTheme.typography.headlineMedium.copy(
|
Column(
|
||||||
fontWeight = FontWeight.Normal,
|
Modifier
|
||||||
fontStyle = FontStyle.Italic,
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
letterSpacing = 1.5.sp,
|
.drawBehind {
|
||||||
),
|
drawRect(
|
||||||
color = Color.White,
|
color = YarisRed,
|
||||||
maxLines = 1,
|
topLeft = Offset(0f, size.height - 4.dp.toPx()),
|
||||||
overflow = TextOverflow.Ellipsis,
|
size = Size(size.width, 3.dp.toPx()),
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
)
|
||||||
)
|
}
|
||||||
Spacer(Modifier.height(1.dp))
|
) {
|
||||||
// Red accent underline — 48dp wide
|
Text(
|
||||||
Canvas(
|
"YARIS-OBD",
|
||||||
Modifier
|
style = MaterialTheme.typography.titleLarge.copy(
|
||||||
.padding(start = 16.dp)
|
fontWeight = FontWeight.Normal,
|
||||||
.width(48.dp)
|
fontStyle = FontStyle.Italic,
|
||||||
.height(3.dp)
|
letterSpacing = 1.2.sp,
|
||||||
) {
|
),
|
||||||
drawRect(color = YarisRed)
|
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.compose.ui.platform.LocalView
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
// Brand colors — matching web UI: #1c1c1e sidebar, #e63946 red, green primary
|
// Brand colors — matching static/yaris.css (mobile web UI)
|
||||||
private val YarisGreen = Color(0xFF4CAF50)
|
val YarisRed = Color(0xFFE63946) // --accent
|
||||||
private val YarisGreenDark = Color(0xFF388E3C)
|
val YarisDark = Color(0xFF1C1C1E) // --sidebar-bg (top bar + bottom nav)
|
||||||
val YarisRed = Color(0xFFE63946)
|
val YarisBlue = Color(0xFF4E73DF) // --primary
|
||||||
private val YarisBackground = Color(0xFF1C1C1E)
|
val YarisBody = Color(0xFFF4F6FB) // body background
|
||||||
private val YarisSurface = Color(0xFF2A2A2C)
|
val YarisMuted = Color(0xFF6C757D) // text-muted
|
||||||
private val YarisSurfaceVariant = Color(0xFF3A3A3C)
|
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val LightColorScheme = lightColorScheme(
|
||||||
primary = YarisGreen,
|
primary = YarisBlue,
|
||||||
onPrimary = Color.Black,
|
onPrimary = Color.White,
|
||||||
primaryContainer = YarisGreenDark,
|
primaryContainer = Color(0xFFD6E0FB),
|
||||||
secondary = Color(0xFF81C784),
|
onPrimaryContainer = Color(0xFF1B2A5A),
|
||||||
background = YarisBackground,
|
secondary = YarisBlue,
|
||||||
surface = YarisSurface,
|
onSecondary = Color.White,
|
||||||
surfaceContainerHigh = YarisSurfaceVariant,
|
secondaryContainer = Color(0xFFE8EDFB),
|
||||||
onBackground = Color(0xFFE0E0E0),
|
onSecondaryContainer = Color(0xFF1B2A5A),
|
||||||
onSurface = Color(0xFFE0E0E0),
|
tertiary = Color(0xFF36B9CC),
|
||||||
onSurfaceVariant = Color(0xFFAAAAAA),
|
onTertiary = Color.White,
|
||||||
error = Color(0xFFEF5350),
|
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 DarkColorScheme = darkColorScheme(
|
||||||
private val LightColorScheme = lightColorScheme(
|
primary = YarisBlue,
|
||||||
primary = YarisGreenDark,
|
|
||||||
onPrimary = Color.White,
|
onPrimary = Color.White,
|
||||||
primaryContainer = Color(0xFFC8E6C9),
|
background = YarisDark,
|
||||||
secondary = Color(0xFF43A047),
|
onBackground = Color(0xFFE0E0E0),
|
||||||
background = Color(0xFFFAFAFA),
|
surface = YarisDark,
|
||||||
surface = Color.White,
|
onSurface = Color(0xFFE0E0E0),
|
||||||
surfaceContainerHigh = Color(0xFFF5F5F5),
|
surfaceContainerHigh = Color(0xFF2A2A2C),
|
||||||
onBackground = Color(0xFF212121),
|
onSurfaceVariant = Color(0xFFAAAAAA),
|
||||||
onSurface = Color(0xFF212121),
|
error = YarisRed,
|
||||||
onSurfaceVariant = Color(0xFF757575),
|
|
||||||
error = Color(0xFFD32F2F),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun YarisOBDTheme(
|
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
|
dynamicColor: Boolean = false, // Off by default — use our brand colors
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -72,8 +82,9 @@ fun YarisOBDTheme(
|
|||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
val window = (view.context as Activity).window
|
val window = (view.context as Activity).window
|
||||||
window.statusBarColor = colorScheme.background.toArgb()
|
window.statusBarColor = YarisDark.toArgb()
|
||||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
// Dark top bar sits under the status bar, so status icons stay light.
|
||||||
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-4
@@ -227,10 +227,32 @@ android/
|
|||||||
(`PORTAL_TILE_URL`, zoom 0–19) so it works over cellular away from the hotspot
|
(`PORTAL_TILE_URL`, zoom 0–19) so it works over cellular away from the hotspot
|
||||||
- [ ] Video playback (stream endpoint) — deferred
|
- [ ] Video playback (stream endpoint) — deferred
|
||||||
|
|
||||||
### 28.7 — Polish + distribution ⬜ NOT STARTED
|
### 28.7 — UI redesign: match the mobile web UI ✅ DONE (2026-08-15)
|
||||||
**Depends on: 28.1–28.6 (whichever subset ships).**
|
**Depends on: 28.1–28.6. Renumbers the old 28.7 (polish) to 28.8.**
|
||||||
|
|
||||||
#### 28.7.1 — App polish
|
#### 28.7.1 — Theme
|
||||||
|
- [x] Light theme matching `static/yaris.css`: content bg `#f4f6fb`, white cards,
|
||||||
|
primary `#4e73df`, accent/error red `#e63946`, text-muted `#6c757d`
|
||||||
|
- [x] App defaults to light (matches the web; the web has no dark mode)
|
||||||
|
|
||||||
|
#### 28.7.2 — Chrome (top bar + bottom nav)
|
||||||
|
- [x] Global dark top bar (`YarisHeader`): `#1c1c1e`, white italic "YARIS-OBD"
|
||||||
|
wordmark + red underline, status-bar aware
|
||||||
|
- [x] Dark bottom `NavigationBar` (`#1c1c1e`, white active / 55% white inactive)
|
||||||
|
mirroring the web's `.mobile-bottom-nav`
|
||||||
|
- [x] Removed the per-screen `YarisHeader` in favour of the one global top bar
|
||||||
|
- [x] Trip replay screen gets the same dark top bar
|
||||||
|
|
||||||
|
#### 28.7.3 — Component polish (follow-up)
|
||||||
|
- [ ] Card shadows (`shadow-sm`) + white card surfaces
|
||||||
|
- [ ] Blue primary buttons (`btn-primary` #4e73df)
|
||||||
|
- [ ] FontAwesome-style icons where they map to the web nav
|
||||||
|
- [ ] Page titles (`h3 fw-bold`) at the top of each screen
|
||||||
|
|
||||||
|
### 28.8 — Polish + distribution ⬜ NOT STARTED
|
||||||
|
**Depends on: 28.1–28.7 (whichever subset ships).**
|
||||||
|
|
||||||
|
#### 28.8.1 — App polish
|
||||||
- [ ] App icon (adaptive icon: foreground + background layers)
|
- [ ] App icon (adaptive icon: foreground + background layers)
|
||||||
- [ ] Splash screen (Android 12+ SplashScreen API)
|
- [ ] Splash screen (Android 12+ SplashScreen API)
|
||||||
- [ ] Dark theme verified on all screens (default, car-friendly)
|
- [ ] Dark theme verified on all screens (default, car-friendly)
|
||||||
@@ -238,7 +260,7 @@ android/
|
|||||||
- [ ] Accessibility: content descriptions on icons, reasonable touch targets (≥48dp)
|
- [ ] Accessibility: content descriptions on icons, reasonable touch targets (≥48dp)
|
||||||
- [ ] Commit: `feat: app polish`
|
- [ ] Commit: `feat: app polish`
|
||||||
|
|
||||||
#### 28.7.2 — Build + distribution
|
#### 28.8.2 — Build + distribution
|
||||||
- [ ] Release build signing key (keystore generated, not committed)
|
- [ ] Release build signing key (keystore generated, not committed)
|
||||||
- [ ] ProGuard/R8 rules verified (Retrofit, OkHttp, Coil, MapLibre keep rules)
|
- [ ] ProGuard/R8 rules verified (Retrofit, OkHttp, Coil, MapLibre keep rules)
|
||||||
- [ ] APK output: `app/build/outputs/apk/release/app-release.apk`
|
- [ ] APK output: `app/build/outputs/apk/release/app-release.apk`
|
||||||
|
|||||||
Reference in New Issue
Block a user