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:
@@ -24,6 +24,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 java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
@@ -38,6 +39,7 @@ fun HomeScreen(
|
||||
vm: HomeViewModel = hiltViewModel(),
|
||||
) {
|
||||
val pairs by vm.syncPairs.collectAsState()
|
||||
val progressMap by vm.syncProgressMap.collectAsState()
|
||||
|
||||
if (pairs.isEmpty()) {
|
||||
EmptyState(modifier = modifier.fillMaxSize(), onAdd = onAddPair)
|
||||
@@ -50,6 +52,7 @@ fun HomeScreen(
|
||||
items(pairs, key = { it.id }) { pair ->
|
||||
SyncPairCard(
|
||||
pair = pair,
|
||||
progress = progressMap[pair.id],
|
||||
onClick = { onPairClick(pair.id) },
|
||||
onSync = { vm.triggerSync(pair) },
|
||||
onToggle = { vm.toggleEnabled(pair) },
|
||||
@@ -64,6 +67,7 @@ fun HomeScreen(
|
||||
@Composable
|
||||
private fun SyncPairCard(
|
||||
pair: SyncPairEntity,
|
||||
progress: SyncProgress? = null,
|
||||
onClick: () -> Unit,
|
||||
onSync: () -> Unit,
|
||||
onToggle: () -> Unit,
|
||||
@@ -176,7 +180,7 @@ private fun SyncPairCard(
|
||||
SyncStatus.SYNCING -> FilledTonalIconButton(onClick = onPause, modifier = Modifier.size(36.dp)) {
|
||||
Icon(Icons.Default.Pause, "Pause sync", modifier = Modifier.size(18.dp))
|
||||
}
|
||||
SyncStatus.PAUSED -> FilledTonalIconButton(onClick = onSync, modifier = Modifier.size(36.dp)) {
|
||||
SyncStatus.PAUSED -> FilledTonalIconButton(onClick = onSync, modifier = Modifier.size(36.dp), enabled = pair.isEnabled) {
|
||||
Icon(Icons.Default.PlayArrow, "Resume sync", modifier = Modifier.size(18.dp))
|
||||
}
|
||||
else -> FilledTonalIconButton(onClick = onSync, modifier = Modifier.size(36.dp)) {
|
||||
@@ -184,6 +188,47 @@ private fun SyncPairCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
} else if (displayProgress != null) {
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
if (displayProgress.uploaded > 0) {
|
||||
Icon(Icons.Default.ArrowUpward, null, Modifier.size(11.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
Text("${displayProgress.uploaded}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
if (displayProgress.downloaded > 0) {
|
||||
Icon(Icons.Default.ArrowDownward, null, Modifier.size(11.dp), tint = MaterialTheme.colorScheme.secondary)
|
||||
Text("${displayProgress.downloaded}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
if (displayProgress.deleted > 0) {
|
||||
Icon(Icons.Default.DeleteOutline, null, Modifier.size(11.dp), tint = MaterialTheme.colorScheme.error)
|
||||
Text("${displayProgress.deleted}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
if (displayProgress.bytesTransferred > 0) {
|
||||
Text(
|
||||
"· ${displayProgress.bytesTransferred.toDisplaySize()}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,6 +296,13 @@ private fun EmptyState(modifier: Modifier = Modifier, onAdd: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
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 val SyncStatus.accentColor: Color
|
||||
@Composable get() = when (this) {
|
||||
SyncStatus.SUCCESS -> Color(0xFF2E7D32)
|
||||
|
||||
Reference in New Issue
Block a user