feat: server-presence indicator on Downloads tab

- CredentialStore tracks uploaded filenames in prefs
- UploadWorker success marks file as uploaded
- Downloads tab shows green CloudDone icon next to files already on server
- Upload button skipped for files already on server (dedup)
This commit is contained in:
Mathieu Moran
2026-08-12 15:42:51 -04:00
parent 0c81f1835d
commit 5b3c04b83d
4 changed files with 28 additions and 1 deletions
@@ -94,6 +94,10 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
private val _uploadProgress = MutableStateFlow<Map<String, ActiveDownload>>(emptyMap()) private val _uploadProgress = MutableStateFlow<Map<String, ActiveDownload>>(emptyMap())
val uploadProgress: StateFlow<Map<String, ActiveDownload>> = _uploadProgress.asStateFlow() val uploadProgress: StateFlow<Map<String, ActiveDownload>> = _uploadProgress.asStateFlow()
// Files already on server (filename set)
private val _uploadedNames = MutableStateFlow<Set<String>>(emptySet())
val uploadedNames: StateFlow<Set<String>> = _uploadedNames.asStateFlow()
// USB dashcam state // USB dashcam state
private val _dashcamFiles = MutableStateFlow<List<DashcamFile>>(emptyList()) private val _dashcamFiles = MutableStateFlow<List<DashcamFile>>(emptyList())
val dashcamFiles: StateFlow<List<DashcamFile>> = _dashcamFiles.asStateFlow() val dashcamFiles: StateFlow<List<DashcamFile>> = _dashcamFiles.asStateFlow()
@@ -117,6 +121,7 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
init { init {
loadVideos() loadVideos()
scanDownloads() scanDownloads()
_uploadedNames.value = CredentialStore.getUploaded(getApplication())
} }
fun loadVideos() { fun loadVideos() {
@@ -318,6 +323,8 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
when (info.state) { when (info.state) {
WorkInfo.State.SUCCEEDED -> { WorkInfo.State.SUCCEEDED -> {
CredentialStore.markUploaded(getApplication(), file.name)
_uploadedNames.value = CredentialStore.getUploaded(getApplication())
viewModelScope.launch { _downloadEvents.emit("✓ Uploaded: ${file.name}") } viewModelScope.launch { _downloadEvents.emit("✓ Uploaded: ${file.name}") }
_uploadProgress.value = _uploadProgress.value - file.name _uploadProgress.value = _uploadProgress.value - file.name
} }
@@ -32,6 +32,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudUpload import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.CloudDone
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -61,6 +62,7 @@ fun DownloadsScreen(
activeUploads: Map<String, ActiveDownload>, activeUploads: Map<String, ActiveDownload>,
deletingNames: Set<String>, deletingNames: Set<String>,
credentialsConfigured: Boolean, credentialsConfigured: Boolean,
uploadedNames: Set<String>,
onDelete: (DownloadedFile) -> Unit, onDelete: (DownloadedFile) -> Unit,
onCancel: (ActiveDownload) -> Unit, onCancel: (ActiveDownload) -> Unit,
onUpload: (DownloadedFile) -> Unit, onUpload: (DownloadedFile) -> Unit,
@@ -214,7 +216,13 @@ fun DownloadsScreen(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text(file.name, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis) Row(verticalAlignment = Alignment.CenterVertically) {
Text(file.name, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false))
if (file.name in uploadedNames) {
Spacer(Modifier.width(6.dp))
Icon(Icons.Filled.CloudDone, "On server", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp))
}
}
Text(formatSize(file.size), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(formatSize(file.size), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
} }
if (isDeleting) { if (isDeleting) {
@@ -62,6 +62,7 @@ fun YarisMainScreen(client: OkHttpClient) {
val activeDownloads by dashcamVm.activeDownloads.collectAsState() val activeDownloads by dashcamVm.activeDownloads.collectAsState()
val deletingNames by dashcamVm.deletingNames.collectAsState() val deletingNames by dashcamVm.deletingNames.collectAsState()
val uploadProgress by dashcamVm.uploadProgress.collectAsState() val uploadProgress by dashcamVm.uploadProgress.collectAsState()
val uploadedNames by dashcamVm.uploadedNames.collectAsState()
val dashcamFiles by dashcamVm.dashcamFiles.collectAsState() val dashcamFiles by dashcamVm.dashcamFiles.collectAsState()
val dashcamTotalSize by dashcamVm.dashcamTotalSize.collectAsState() val dashcamTotalSize by dashcamVm.dashcamTotalSize.collectAsState()
val dashcamStatus by dashcamVm.dashcamStatus.collectAsState() val dashcamStatus by dashcamVm.dashcamStatus.collectAsState()
@@ -129,6 +130,7 @@ fun YarisMainScreen(client: OkHttpClient) {
activeUploads = uploadProgress, activeUploads = uploadProgress,
deletingNames = deletingNames, deletingNames = deletingNames,
credentialsConfigured = credentialsConfigured, credentialsConfigured = credentialsConfigured,
uploadedNames = uploadedNames,
onDelete = { dashcamVm.deleteDownloaded(it) }, onDelete = { dashcamVm.deleteDownloaded(it) },
onCancel = { dashcamVm.cancelDownload(it) }, onCancel = { dashcamVm.cancelDownload(it) },
onUpload = { dashcamVm.uploadFile(it) }, onUpload = { dashcamVm.uploadFile(it) },
@@ -7,6 +7,7 @@ object CredentialStore {
private const val KEY_SERVER_URL = "server_url" private const val KEY_SERVER_URL = "server_url"
private const val KEY_DEVICE_NAME = "device_name" private const val KEY_DEVICE_NAME = "device_name"
private const val KEY_API_KEY = "api_key" private const val KEY_API_KEY = "api_key"
private const val KEY_UPLOADED = "uploaded_files"
private fun prefs(context: Context) = private fun prefs(context: Context) =
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
@@ -25,4 +26,13 @@ object CredentialStore {
.putString(KEY_API_KEY, apiKey) .putString(KEY_API_KEY, apiKey)
.apply() .apply()
} }
fun markUploaded(context: Context, filename: String) {
val set = getUploaded(context).toMutableSet()
set.add(filename)
prefs(context).edit().putStringSet(KEY_UPLOADED, set).apply()
}
fun getUploaded(context: Context): Set<String> =
prefs(context).getStringSet(KEY_UPLOADED, emptySet()) ?: emptySet()
} }