feat(android): upload all + per-file retry (Phase 28.3 complete)
This commit is contained in:
@@ -101,6 +101,10 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
private val _uploadedNames = MutableStateFlow<Set<String>>(emptySet())
|
||||
val uploadedNames: StateFlow<Set<String>> = _uploadedNames.asStateFlow()
|
||||
|
||||
// Files whose last upload attempt failed (drives the per-file Retry button)
|
||||
private val _failedUploads = MutableStateFlow<Set<String>>(emptySet())
|
||||
val failedUploads: StateFlow<Set<String>> = _failedUploads.asStateFlow()
|
||||
|
||||
// USB dashcam state
|
||||
private val _dashcamFiles = MutableStateFlow<List<DashcamFile>>(emptyList())
|
||||
val dashcamFiles: StateFlow<List<DashcamFile>> = _dashcamFiles.asStateFlow()
|
||||
@@ -316,6 +320,9 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear any previous failure marker — this is a fresh attempt
|
||||
_failedUploads.value = _failedUploads.value - file.name
|
||||
|
||||
val serverUrl = CredentialStore.getServerUrl(ctx)
|
||||
val deviceName = CredentialStore.getDeviceName(ctx)
|
||||
val apiKey = CredentialStore.getApiKey(ctx)
|
||||
@@ -350,11 +357,13 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
CredentialStore.markUploaded(getApplication(), file.name)
|
||||
_uploadedNames.value = CredentialStore.getUploaded(getApplication())
|
||||
_failedUploads.value = _failedUploads.value - file.name
|
||||
viewModelScope.launch { _downloadEvents.emit("✓ Uploaded: ${file.name}") }
|
||||
_uploadProgress.value = _uploadProgress.value - file.name
|
||||
}
|
||||
WorkInfo.State.FAILED -> {
|
||||
val err = info.outputData.getString("error") ?: "Unknown error"
|
||||
_failedUploads.value = _failedUploads.value + file.name
|
||||
viewModelScope.launch { _downloadEvents.emit("✗ Upload failed: $err") }
|
||||
_uploadProgress.value = _uploadProgress.value - file.name
|
||||
}
|
||||
@@ -368,6 +377,23 @@ class DashcamViewModel(application: Application) : AndroidViewModel(application)
|
||||
viewModelScope.launch { _downloadEvents.emit("⏳ Uploading: ${file.name}") }
|
||||
}
|
||||
|
||||
fun uploadAll() {
|
||||
val ctx = getApplication<YarisApp>().applicationContext
|
||||
if (!CredentialStore.isConfigured(ctx)) {
|
||||
viewModelScope.launch { _downloadEvents.emit("✗ Server credentials not configured") }
|
||||
return
|
||||
}
|
||||
val pending = _downloadedFiles.value.filter { f ->
|
||||
f.name !in _uploadedNames.value && f.name !in _uploadProgress.value
|
||||
}
|
||||
if (pending.isEmpty()) {
|
||||
viewModelScope.launch { _downloadEvents.emit("✓ Nothing to upload — all files already on server") }
|
||||
return
|
||||
}
|
||||
pending.forEach { uploadFile(it) }
|
||||
viewModelScope.launch { _downloadEvents.emit("⏳ Queued ${pending.size} file(s) for upload") }
|
||||
}
|
||||
|
||||
fun cancelUpload(dl: ActiveDownload) {
|
||||
try {
|
||||
val uuid = UUID.fromString(dl.workerId)
|
||||
|
||||
@@ -63,6 +63,8 @@ fun DownloadsScreen(
|
||||
deletingNames: Set<String>,
|
||||
credentialsConfigured: Boolean,
|
||||
uploadedNames: Set<String>,
|
||||
failedUploads: Set<String>,
|
||||
onUploadAll: () -> Unit,
|
||||
onDelete: (DownloadedFile) -> Unit,
|
||||
onCancel: (ActiveDownload) -> Unit,
|
||||
onUpload: (DownloadedFile) -> Unit,
|
||||
@@ -85,13 +87,30 @@ fun DownloadsScreen(
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text("${files.size} files · Movies/yaris-obd/", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Button(
|
||||
onClick = onGenerateTestFile,
|
||||
modifier = Modifier.height(34.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp)
|
||||
) {
|
||||
Text("Create 120 MB test file", style = MaterialTheme.typography.bodySmall)
|
||||
val pendingCount = files.count { it.name !in uploadedNames && it.name !in activeUploads.keys }
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (credentialsConfigured) {
|
||||
Button(
|
||||
onClick = onUploadAll,
|
||||
enabled = pendingCount > 0,
|
||||
modifier = Modifier.height(34.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp)
|
||||
) {
|
||||
Text(
|
||||
if (pendingCount > 0) "Upload all ($pendingCount)" else "Upload all",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = onGenerateTestFile,
|
||||
modifier = Modifier.height(34.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp)
|
||||
) {
|
||||
Text("Create 120 MB test file", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,7 +266,17 @@ fun DownloadsScreen(
|
||||
if (isDeleting) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.error)
|
||||
} else {
|
||||
if (credentialsConfigured && file.name !in uploadedNames) {
|
||||
val isUploading = file.name in activeUploads.keys
|
||||
if (file.name in failedUploads) {
|
||||
Button(
|
||||
onClick = { onUpload(file) },
|
||||
enabled = !isUploading,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
||||
modifier = Modifier.height(30.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp)
|
||||
) { Text("Retry", style = MaterialTheme.typography.bodySmall) }
|
||||
} else if (credentialsConfigured && file.name !in uploadedNames && !isUploading) {
|
||||
IconButton(onClick = { onUpload(file) }) {
|
||||
Icon(Icons.Filled.CloudUpload, "Upload", tint = MaterialTheme.colorScheme.tertiary)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ fun YarisMainScreen(client: OkHttpClient) {
|
||||
val deletingNames by dashcamVm.deletingNames.collectAsState()
|
||||
val uploadProgress by dashcamVm.uploadProgress.collectAsState()
|
||||
val uploadedNames by dashcamVm.uploadedNames.collectAsState()
|
||||
val failedUploads by dashcamVm.failedUploads.collectAsState()
|
||||
val dashcamFiles by dashcamVm.dashcamFiles.collectAsState()
|
||||
val dashcamTotalSize by dashcamVm.dashcamTotalSize.collectAsState()
|
||||
val dashcamStatus by dashcamVm.dashcamStatus.collectAsState()
|
||||
@@ -131,6 +132,8 @@ fun YarisMainScreen(client: OkHttpClient) {
|
||||
deletingNames = deletingNames,
|
||||
credentialsConfigured = credentialsConfigured,
|
||||
uploadedNames = uploadedNames,
|
||||
failedUploads = failedUploads,
|
||||
onUploadAll = { dashcamVm.uploadAll() },
|
||||
onDelete = { dashcamVm.deleteDownloaded(it) },
|
||||
onCancel = { dashcamVm.cancelDownload(it) },
|
||||
onUpload = { dashcamVm.uploadFile(it) },
|
||||
|
||||
+4
-4
@@ -147,12 +147,12 @@ android/
|
||||
- [x] Debug logging (`Log.d`/`Log.e`, tag `UploadWorker`) throughout
|
||||
- [x] Re-upload guard: skips files already on server (`already_uploaded`)
|
||||
|
||||
#### 28.3.3 — Upload queue UI 🚧 IN PROGRESS
|
||||
#### 28.3.3 — Upload queue UI ✅ DONE (2026-08-15)
|
||||
- [x] Downloads screen shows per-file upload status + inline progress bar
|
||||
- [x] In-app test file generator for upload testing
|
||||
- [ ] "Upload all" button → enqueue all pending files
|
||||
- [ ] Retry button per failed file
|
||||
- [ ] Commit: `feat: upload queue screen`
|
||||
- [x] "Upload all" button → enqueue all pending files (header card, shows pending count)
|
||||
- [x] Retry button per failed file (tracks failed uploads via `failedUploads` StateFlow)
|
||||
- [x] Commit: `feat: upload queue screen — upload all + per-file retry`
|
||||
|
||||
### 28.4 — Live dashboard ⬜ NOT STARTED
|
||||
**Depends on: 28.1 (networking layer). Independent of 28.2/28.3.**
|
||||
|
||||
Reference in New Issue
Block a user