feat: USB dashcam file browser on Dashcam tab
- PiApi extended: listDashcamFiles, deleteDashcamFile, importDashcamFiles - DTO: DashcamFilesResponse, DashcamDevice, DashcamFile - DashcamScreen: USB Dashcam card with status, file count, Import All + Refresh - ViewModel: loadDashcamFiles, deleteDashcamFile, importDashcamFiles
This commit is contained in:
@@ -12,6 +12,7 @@ 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.PaddingValues
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.yarisobd.app.data.remote.dto.DashcamFile
|
||||
import com.yarisobd.app.data.remote.dto.VideoFile
|
||||
|
||||
@Composable
|
||||
@@ -50,13 +52,46 @@ fun DashcamScreen(
|
||||
isLoading: Boolean,
|
||||
error: String?,
|
||||
downloadProgress: Map<String, DownloadProgress>,
|
||||
dashcamFiles: List<DashcamFile>,
|
||||
dashcamTotalSize: Long,
|
||||
dashcamStatus: String,
|
||||
dashcamLoading: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
onDownload: (VideoFile) -> Unit,
|
||||
onDelete: (VideoFile) -> Unit,
|
||||
onDeleteDashcam: (DashcamFile) -> Unit,
|
||||
onImport: (List<String>?) -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
com.yarisobd.app.ui.components.YarisHeader()
|
||||
|
||||
// USB Dashcam section
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text("USB Dashcam", style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary)
|
||||
when (dashcamStatus) {
|
||||
"ok" -> {
|
||||
Text("${dashcamFiles.size} files · ${formatBytes(dashcamTotalSize)}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
if (dashcamFiles.isNotEmpty()) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row {
|
||||
Button(onClick = { onImport(null) }, modifier = Modifier.weight(1f), contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)) { Text("Import All", style = MaterialTheme.typography.bodySmall) }
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Button(onClick = onRefresh, enabled = !dashcamLoading, modifier = Modifier.weight(1f), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary), contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)) { Text(if (dashcamLoading) "..." else "Refresh", style = MaterialTheme.typography.bodySmall) }
|
||||
}
|
||||
}
|
||||
}
|
||||
"no_device" -> Text("No USB dashcam connected", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error)
|
||||
"error" -> Text("Failed to read dashcam", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error)
|
||||
"" -> Text("Tap Refresh to scan", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
else -> Text(dashcamStatus, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header card
|
||||
Card(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -10,6 +10,8 @@ import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import com.yarisobd.app.data.CredentialStore
|
||||
import com.yarisobd.app.data.remote.PiApi
|
||||
import com.yarisobd.app.data.remote.dto.DashcamFile
|
||||
import com.yarisobd.app.data.remote.dto.DashcamFilesResponse
|
||||
import com.yarisobd.app.data.remote.dto.VideoFile
|
||||
import com.yarisobd.app.data.remote.dto.VideoListResponse
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -92,6 +94,16 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
private val _uploadProgress = MutableStateFlow<Map<String, ActiveDownload>>(emptyMap())
|
||||
val uploadProgress: StateFlow<Map<String, ActiveDownload>> = _uploadProgress.asStateFlow()
|
||||
|
||||
// USB dashcam state
|
||||
private val _dashcamFiles = MutableStateFlow<List<DashcamFile>>(emptyList())
|
||||
val dashcamFiles: StateFlow<List<DashcamFile>> = _dashcamFiles.asStateFlow()
|
||||
private val _dashcamTotalSize = MutableStateFlow(0L)
|
||||
val dashcamTotalSize: StateFlow<Long> = _dashcamTotalSize.asStateFlow()
|
||||
private val _dashcamStatus = MutableStateFlow("")
|
||||
val dashcamStatus: StateFlow<String> = _dashcamStatus.asStateFlow()
|
||||
private val _dashcamLoading = MutableStateFlow(false)
|
||||
val dashcamLoading: StateFlow<Boolean> = _dashcamLoading.asStateFlow()
|
||||
|
||||
private fun getDownloadDir(): File {
|
||||
// Public Movies/yaris-obd/ — visible in file managers
|
||||
val dir = File(
|
||||
@@ -124,6 +136,46 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDashcamFiles() {
|
||||
viewModelScope.launch {
|
||||
_dashcamLoading.value = true
|
||||
try {
|
||||
val resp: DashcamFilesResponse = api.listDashcamFiles()
|
||||
_dashcamStatus.value = resp.status
|
||||
_dashcamFiles.value = resp.files
|
||||
_dashcamTotalSize.value = resp.total_size
|
||||
} catch (e: Exception) {
|
||||
_dashcamStatus.value = "error"
|
||||
_downloadEvents.emit("Dashcam: ${e.message}")
|
||||
} finally {
|
||||
_dashcamLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDashcamFile(file: DashcamFile) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
api.deleteDashcamFile(mapOf("path" to file.path))
|
||||
_downloadEvents.emit("✓ Deleted from dashcam: ${file.path}")
|
||||
loadDashcamFiles()
|
||||
} catch (e: Exception) {
|
||||
_downloadEvents.emit("✗ Delete failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importDashcamFiles(paths: List<String>?) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
api.importDashcamFiles(mapOf("paths" to (paths ?: emptyList())))
|
||||
_downloadEvents.emit("⏳ Import started")
|
||||
} catch (e: Exception) {
|
||||
_downloadEvents.emit("✗ Import failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadVideo(video: VideoFile) {
|
||||
val url = "$baseUrl/api/local-video/file?path=${video.path}"
|
||||
val filename = video.path.substringAfterLast("/")
|
||||
|
||||
@@ -62,6 +62,10 @@ fun YarisMainScreen(client: OkHttpClient) {
|
||||
val activeDownloads by dashcamVm.activeDownloads.collectAsState()
|
||||
val deletingNames by dashcamVm.deletingNames.collectAsState()
|
||||
val uploadProgress by dashcamVm.uploadProgress.collectAsState()
|
||||
val dashcamFiles by dashcamVm.dashcamFiles.collectAsState()
|
||||
val dashcamTotalSize by dashcamVm.dashcamTotalSize.collectAsState()
|
||||
val dashcamStatus by dashcamVm.dashcamStatus.collectAsState()
|
||||
val dashcamLoading by dashcamVm.dashcamLoading.collectAsState()
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val credentialsConfigured = com.yarisobd.app.data.CredentialStore.isConfigured(context)
|
||||
|
||||
@@ -109,9 +113,15 @@ fun YarisMainScreen(client: OkHttpClient) {
|
||||
isLoading = isLoading,
|
||||
error = error,
|
||||
downloadProgress = downloadProgress,
|
||||
onRefresh = { dashcamVm.loadVideos() },
|
||||
dashcamFiles = dashcamFiles,
|
||||
dashcamTotalSize = dashcamTotalSize,
|
||||
dashcamStatus = dashcamStatus,
|
||||
dashcamLoading = dashcamLoading,
|
||||
onRefresh = { dashcamVm.loadVideos(); dashcamVm.loadDashcamFiles() },
|
||||
onDownload = { dashcamVm.downloadVideo(it) },
|
||||
onDelete = { dashcamVm.deleteVideo(it) },
|
||||
onDeleteDashcam = { dashcamVm.deleteDashcamFile(it) },
|
||||
onImport = { dashcamVm.importDashcamFiles(it) },
|
||||
)
|
||||
1 -> DownloadsScreen(
|
||||
files = downloadedFiles,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.yarisobd.app.data.remote
|
||||
|
||||
import com.yarisobd.app.data.remote.dto.DashcamFilesResponse
|
||||
import com.yarisobd.app.data.remote.dto.GpsStatus
|
||||
import com.yarisobd.app.data.remote.dto.VideoListResponse
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -7,6 +8,7 @@ import okhttp3.ResponseBody
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.HTTP
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.Streaming
|
||||
|
||||
@@ -33,6 +35,20 @@ interface PiApi {
|
||||
suspend fun getThumbnail(@Query("path") path: String): ResponseBody
|
||||
|
||||
/** Delete local copy only */
|
||||
|
||||
// --- USB dashcam endpoints ---
|
||||
|
||||
/** List files on USB dashcam (Phase 16) */
|
||||
@GET("/api/dash/files")
|
||||
suspend fun listDashcamFiles(): DashcamFilesResponse
|
||||
|
||||
/** Delete a single file from USB dashcam */
|
||||
@HTTP(method = "DELETE", path = "/api/dash/file", hasBody = true)
|
||||
suspend fun deleteDashcamFile(@Body body: Map<String, String>): JsonObject
|
||||
|
||||
/** Import files from USB dashcam (selected or all) */
|
||||
@POST("/api/dash/import")
|
||||
suspend fun importDashcamFiles(@Body body: Map<String, List<String>>): JsonObject
|
||||
@HTTP(method = "DELETE", path = "/api/local-video", hasBody = true)
|
||||
suspend fun deleteVideo(@Body body: Map<String, String>): JsonObject
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yarisobd.app.data.remote.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DashcamFilesResponse(
|
||||
val status: String = "",
|
||||
val message: String? = null,
|
||||
val device: DashcamDevice? = null,
|
||||
val files: List<DashcamFile> = emptyList(),
|
||||
val count: Int = 0,
|
||||
val total_size: Long = 0,
|
||||
val errors: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DashcamDevice(
|
||||
val path: String = "",
|
||||
val fstype: String = "",
|
||||
val label: String? = null,
|
||||
val size: String? = null,
|
||||
val mountpoint: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DashcamFile(
|
||||
val path: String = "",
|
||||
val size: Long = 0,
|
||||
val modified: Double = 0.0,
|
||||
)
|
||||
Reference in New Issue
Block a user