feat: portal-wide device selector — session-based, all pages now use selected device

- _portal_device_id() reads from session, falls back to DEVICE_NAME env var
- GET/POST /api/v1/portal/select-device for selector data + set
- Dropdown in server base.html topbar, JS populates from API, page reloads on change
- All portal pages (overview, trips, dashcam, DTC, device settings) now use selected device
This commit is contained in:
Mathieu Moran
2026-08-12 01:40:51 -04:00
parent 1556c65131
commit e30b85d218
6 changed files with 133 additions and 87 deletions
@@ -21,6 +21,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -43,6 +44,7 @@ fun DashcamScreen(
retentionDays: Int,
isLoading: Boolean,
error: String?,
downloadProgress: Map<String, DownloadProgress>,
onRefresh: () -> Unit,
onDownload: (VideoFile) -> Unit,
onDelete: (VideoFile) -> Unit,
@@ -146,8 +148,10 @@ fun DashcamScreen(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(videos, key = { it.path }) { video ->
val progress = downloadProgress[video.path]
VideoRow(
video = video,
downloadProgress = progress,
onDownload = { onDownload(video) },
onDelete = { onDelete(video) },
)
@@ -161,6 +165,7 @@ fun DashcamScreen(
@Composable
private fun VideoRow(
video: VideoFile,
downloadProgress: DownloadProgress?,
onDownload: () -> Unit,
onDelete: () -> Unit,
) {
@@ -224,6 +229,20 @@ private fun VideoRow(
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.tertiary
)
} else if (downloadProgress != null) {
LinearProgressIndicator(
progress = {
if (downloadProgress.totalBytes > 0)
downloadProgress.bytesDownloaded.toFloat() / downloadProgress.totalBytes
else 0f
},
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
)
Text(
text = "Downloading ${downloadProgress.bytesDownloaded / 1_000_000}MB / ${downloadProgress.totalBytes / 1_000_000}MB",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
} else {
Text(
text = if (video.uploaded) "Uploaded" else "Pending",
@@ -12,8 +12,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
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.mutableIntStateOf
@@ -49,10 +52,20 @@ fun YarisMainScreen(client: OkHttpClient) {
val retentionDays by dashcamVm.retentionDays.collectAsState()
val isLoading by dashcamVm.isLoading.collectAsState()
val error by dashcamVm.error.collectAsState()
val downloadProgress by dashcamVm.downloadProgress.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
var selectedTab by remember { mutableIntStateOf(0) }
// Show download/delete events as snackbars
LaunchedEffect(Unit) {
dashcamVm.downloadEvents.collect { event ->
snackbarHostState.showSnackbar(event)
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = {
NavigationBar {
NavigationBarItem(
@@ -78,6 +91,7 @@ fun YarisMainScreen(client: OkHttpClient) {
retentionDays = retentionDays,
isLoading = isLoading,
error = error,
downloadProgress = downloadProgress,
onRefresh = { dashcamVm.loadVideos() },
onDownload = { dashcamVm.downloadVideo(it) },
onDelete = { dashcamVm.deleteVideo(it) },