diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1ce4bcf..522bee7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -36,6 +36,16 @@ + + + + diff --git a/android/app/src/main/java/com/yarisobd/app/DashcamViewModel.kt b/android/app/src/main/java/com/yarisobd/app/DashcamViewModel.kt index 399a8b7..e3785ef 100644 --- a/android/app/src/main/java/com/yarisobd/app/DashcamViewModel.kt +++ b/android/app/src/main/java/com/yarisobd/app/DashcamViewModel.kt @@ -3,6 +3,7 @@ package com.yarisobd.app import android.app.Application import android.content.Context import android.os.Environment +import android.webkit.MimeTypeMap import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.yarisobd.app.data.remote.PiApi @@ -32,6 +33,12 @@ data class DownloadProgress( val totalBytes: Long, ) +data class DownloadedFile( + val name: String, + val size: Long, + val file: File, +) + class DashcamViewModel(application: Application) : AndroidViewModel(application) { private val baseUrl = "http://10.42.0.1" // no trailing slash @@ -66,8 +73,22 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application) private val _downloadEvents = MutableSharedFlow() val downloadEvents = _downloadEvents.asSharedFlow() + private val _downloadedFiles = MutableStateFlow>(emptyList()) + val downloadedFiles: StateFlow> = _downloadedFiles.asStateFlow() + + private fun getDownloadDir(): File { + // Public Movies/yaris-obd/ — visible in file managers + val dir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), + "yaris-obd" + ) + dir.mkdirs() + return dir + } + init { loadVideos() + scanDownloads() } fun loadVideos() { @@ -134,6 +155,7 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application) "✓ Downloaded: ${localFile.name} (${downloaded} bytes)" } _downloadEvents.emit(result) + scanDownloads() } catch (e: Exception) { _downloadProgress.value = _downloadProgress.value - video.path _downloadEvents.emit("✗ Download failed: ${e.message}") @@ -141,6 +163,25 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application) } } + fun scanDownloads() { + val dir = getDownloadDir() + val files = dir.listFiles()?.filter { it.isFile }?.map { f -> + DownloadedFile(name = f.name, size = f.length(), file = f) + }?.sortedByDescending { it.file.lastModified() } ?: emptyList() + _downloadedFiles.value = files + } + + fun deleteDownloaded(file: DownloadedFile) { + viewModelScope.launch { + if (file.file.delete()) { + _downloadEvents.emit("✓ Deleted: ${file.name}") + scanDownloads() + } else { + _downloadEvents.emit("✗ Could not delete: ${file.name}") + } + } + } + fun deleteVideo(video: VideoFile) { viewModelScope.launch { try { diff --git a/android/app/src/main/java/com/yarisobd/app/DownloadsScreen.kt b/android/app/src/main/java/com/yarisobd/app/DownloadsScreen.kt new file mode 100644 index 0000000..664f7ad --- /dev/null +++ b/android/app/src/main/java/com/yarisobd/app/DownloadsScreen.kt @@ -0,0 +1,150 @@ +package com.yarisobd.app + +import android.content.Intent +import androidx.compose.foundation.clickable +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.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 +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.core.content.FileProvider + +@Composable +fun DownloadsScreen( + files: List, + onDelete: (DownloadedFile) -> Unit, + onRefresh: () -> Unit, +) { + val context = LocalContext.current + + Column(modifier = Modifier.fillMaxSize()) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Downloads", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${files.size} files in Movies/yaris-obd/", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (files.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "No downloaded files yet.\nDownload a video from the Dashcam tab.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + LazyColumn( + contentPadding = androidx.compose.foundation.layout.PaddingValues( + start = 16.dp, end = 16.dp, bottom = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(files, key = { it.name }) { file -> + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { + // Open with system video player + val uri = FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + file.file + ) + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "video/*") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(intent) + }, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = file.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = formatBytes(file.size), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Button( + onClick = { onDelete(file) }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ), + modifier = Modifier.height(36.dp) + ) { + Text("✕", style = MaterialTheme.typography.bodySmall) + } + } + } + } + } + } + } +} + +private fun formatBytes(bytes: Long): String { + return when { + bytes >= 1_000_000_000 -> "%.1f GB".format(bytes / 1_000_000_000.0) + bytes >= 1_000_000 -> "%.1f MB".format(bytes / 1_000_000.0) + bytes >= 1_000 -> "%.1f KB".format(bytes / 1_000.0) + else -> "$bytes B" + } +} diff --git a/android/app/src/main/java/com/yarisobd/app/MainActivity.kt b/android/app/src/main/java/com/yarisobd/app/MainActivity.kt index 492ccfe..a3d1926 100644 --- a/android/app/src/main/java/com/yarisobd/app/MainActivity.kt +++ b/android/app/src/main/java/com/yarisobd/app/MainActivity.kt @@ -53,6 +53,7 @@ fun YarisMainScreen(client: OkHttpClient) { val isLoading by dashcamVm.isLoading.collectAsState() val error by dashcamVm.error.collectAsState() val downloadProgress by dashcamVm.downloadProgress.collectAsState() + val downloadedFiles by dashcamVm.downloadedFiles.collectAsState() val snackbarHostState = remember { SnackbarHostState() } var selectedTab by remember { mutableIntStateOf(0) } @@ -77,7 +78,13 @@ fun YarisMainScreen(client: OkHttpClient) { NavigationBarItem( selected = selectedTab == 1, onClick = { selectedTab = 1 }, - icon = { Text("🔌") }, + icon = { Text("�") }, + label = { Text("Downloads") } + ) + NavigationBarItem( + selected = selectedTab == 2, + onClick = { selectedTab = 2 }, + icon = { Text("�🔌") }, label = { Text("Test") } ) } @@ -96,7 +103,12 @@ fun YarisMainScreen(client: OkHttpClient) { onDownload = { dashcamVm.downloadVideo(it) }, onDelete = { dashcamVm.deleteVideo(it) }, ) - 1 -> DualNetworkTestScreen( + 1 -> DownloadsScreen( + files = downloadedFiles, + onDelete = { dashcamVm.deleteDownloaded(it) }, + onRefresh = { dashcamVm.scanDownloads() }, + ) + 2 -> DualNetworkTestScreen( piClient = client, serverClient = client, ) diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..ebbbd14 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + +