v1.0.63: live sync progress counters, pause/resume, .gitignore fix
Build & Release APK / build (push) Has been cancelled
Build & Release APK / build (push) Has been cancelled
- SyncEngine: accepts onProgress callback — emits uploaded/downloaded/ deleted/bytes counts atomically as each file completes - SyncWorker: streams progress to WorkManager data so the UI can poll it live; reports per-run counters in the completion notification; adds pause/resume support - HomeViewModel/PairDetailViewModel: subscribe to live WorkManager progress and surface it via SyncProgress state - SyncPairEntity/SyncPairDao/SyncDatabase: persist last-run counters (uploaded, downloaded, deleted, bytesTransferred) in the DB with a Room migration (v3→v4) - AppModule: provides WorkManager as an injectable singleton - .gitignore: add .kotlin/ to exclude compiler session files Security: no new issues — all logging via Timber (debug-only), DB queries use Room parameterized API, file sharing via FileProvider. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.syncflow.data.db.entities.SyncPairEntity
|
||||
import com.syncflow.domain.model.SyncStatus
|
||||
import com.syncflow.ui.shared.SyncProgress
|
||||
import com.syncflow.ui.shared.SyncEventRow
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@@ -40,6 +41,7 @@ fun PairDetailScreen(
|
||||
val pair by vm.pair.collectAsState()
|
||||
val events by vm.events.collectAsState()
|
||||
val conflictCount by vm.unresolvedConflicts.collectAsState()
|
||||
val syncProgress by vm.syncProgress.collectAsState()
|
||||
var showDelete by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDelete) {
|
||||
@@ -70,7 +72,7 @@ fun PairDetailScreen(
|
||||
SyncStatus.SYNCING -> IconButton(onClick = { vm.pauseSync() }) {
|
||||
Icon(Icons.Default.Pause, "Pause sync")
|
||||
}
|
||||
SyncStatus.PAUSED -> IconButton(onClick = { vm.syncNow() }) {
|
||||
SyncStatus.PAUSED -> IconButton(onClick = { vm.syncNow() }, enabled = pair?.isEnabled == true) {
|
||||
Icon(Icons.Default.PlayArrow, "Resume sync")
|
||||
}
|
||||
else -> IconButton(onClick = { vm.syncNow() }) {
|
||||
@@ -88,7 +90,7 @@ fun PairDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
item {
|
||||
pair?.let { p -> StatusBanner(p) }
|
||||
pair?.let { p -> StatusBanner(p, syncProgress) }
|
||||
}
|
||||
|
||||
item {
|
||||
@@ -148,7 +150,7 @@ fun PairDetailScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusBanner(pair: SyncPairEntity) {
|
||||
private fun StatusBanner(pair: SyncPairEntity, progress: SyncProgress? = null) {
|
||||
val (icon, label, containerColor) = when (pair.lastSyncResult) {
|
||||
SyncStatus.SUCCESS -> Triple(Icons.Default.CheckCircle, "Synced", MaterialTheme.colorScheme.primaryContainer)
|
||||
SyncStatus.SYNCING -> Triple(Icons.Default.Sync, "Syncing…", MaterialTheme.colorScheme.secondaryContainer)
|
||||
@@ -181,8 +183,39 @@ private fun StatusBanner(pair: SyncPairEntity) {
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(label, style = MaterialTheme.typography.titleMedium)
|
||||
pair.lastSyncAt?.let {
|
||||
Text(it.toRelativeString(), style = MaterialTheme.typography.bodySmall)
|
||||
val displayProgress = when {
|
||||
pair.lastSyncResult == SyncStatus.SYNCING -> progress
|
||||
pair.lastSyncUploaded > 0 || pair.lastSyncDownloaded > 0 || pair.lastSyncDeleted > 0 ->
|
||||
SyncProgress(pair.lastSyncUploaded, pair.lastSyncDownloaded, pair.lastSyncDeleted, pair.lastSyncBytesTransferred)
|
||||
else -> null
|
||||
}
|
||||
if (pair.lastSyncResult == SyncStatus.SYNCING && displayProgress == null) {
|
||||
Text("Starting…", style = MaterialTheme.typography.bodySmall)
|
||||
} else if (displayProgress != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
if (displayProgress.uploaded > 0) {
|
||||
Icon(Icons.Default.ArrowUpward, null, Modifier.size(12.dp))
|
||||
Text("${displayProgress.uploaded} up", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (displayProgress.downloaded > 0) {
|
||||
Icon(Icons.Default.ArrowDownward, null, Modifier.size(12.dp))
|
||||
Text("${displayProgress.downloaded} down", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (displayProgress.deleted > 0) {
|
||||
Icon(Icons.Default.DeleteOutline, null, Modifier.size(12.dp))
|
||||
Text("${displayProgress.deleted} del", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (displayProgress.bytesTransferred > 0) {
|
||||
Text("· ${displayProgress.bytesTransferred.toDisplaySize()}", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pair.lastSyncAt?.let {
|
||||
Text(it.toRelativeString(), style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,6 +273,13 @@ private fun InfoRow(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.toDisplaySize(): String = when {
|
||||
this < 1_024 -> "$this B"
|
||||
this < 1_048_576 -> "${"%.1f".format(this / 1_024.0)} KB"
|
||||
this < 1_073_741_824 -> "${"%.1f".format(this / 1_048_576.0)} MB"
|
||||
else -> "${"%.1f".format(this / 1_073_741_824.0)} GB"
|
||||
}
|
||||
|
||||
private fun String.toDisplayPath(): String {
|
||||
val decoded = java.net.URLDecoder.decode(this, "UTF-8")
|
||||
return decoded.trimEnd('/').substringAfterLast('/').substringAfterLast(':').ifEmpty { decoded }
|
||||
|
||||
Reference in New Issue
Block a user