From 21b7ffc7b37ff3d90781049a7cd2a254be4a27f9 Mon Sep 17 00:00:00 2001 From: Amir Khodak Date: Tue, 26 May 2026 01:51:45 +0000 Subject: [PATCH] v1.0.59: pause/resume sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New PAUSED status. When a sync is running, the sync button becomes a pause button (⏸). Tapping it cancels the WorkManager job and sets the status to PAUSED (purple). The button then becomes a play button (▶) to resume. Works in both the home screen card and the pair detail screen. Co-Authored-By: Claude Sonnet 4.6 --- .../com/syncflow/domain/model/SyncPair.kt | 2 +- .../kotlin/com/syncflow/ui/home/HomeScreen.kt | 31 ++++++++++++------- .../com/syncflow/ui/home/HomeViewModel.kt | 6 ++++ .../ui/pairdetail/PairDetailScreen.kt | 13 +++++++- .../ui/pairdetail/PairDetailViewModel.kt | 7 +++++ version.properties | 4 +-- 6 files changed, 47 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/syncflow/domain/model/SyncPair.kt b/app/src/main/kotlin/com/syncflow/domain/model/SyncPair.kt index 845ebe0..a41d86b 100644 --- a/app/src/main/kotlin/com/syncflow/domain/model/SyncPair.kt +++ b/app/src/main/kotlin/com/syncflow/domain/model/SyncPair.kt @@ -70,5 +70,5 @@ enum class ScheduleType(val label: String) { } enum class SyncStatus { - IDLE, SYNCING, SUCCESS, PARTIAL, FAILED, CONFLICT, + IDLE, SYNCING, PAUSED, SUCCESS, PARTIAL, FAILED, CONFLICT, } diff --git a/app/src/main/kotlin/com/syncflow/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/syncflow/ui/home/HomeScreen.kt index 06520c0..97f00b7 100644 --- a/app/src/main/kotlin/com/syncflow/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/syncflow/ui/home/HomeScreen.kt @@ -53,6 +53,7 @@ fun HomeScreen( onClick = { onPairClick(pair.id) }, onSync = { vm.triggerSync(pair) }, onToggle = { vm.toggleEnabled(pair) }, + onPause = { vm.pauseSync(pair) }, ) } item { Spacer(Modifier.height(80.dp)) } @@ -66,6 +67,7 @@ private fun SyncPairCard( onClick: () -> Unit, onSync: () -> Unit, onToggle: () -> Unit, + onPause: () -> Unit = {}, ) { val accentColor = pair.lastSyncResult.accentColor @@ -170,13 +172,16 @@ private fun SyncPairCard( animationSpec = infiniteRepeatable(tween(900, easing = LinearEasing)), label = "cardRotation", ) - FilledTonalIconButton(onClick = onSync, modifier = Modifier.size(36.dp)) { - Icon( - Icons.Default.Sync, "Sync now", - modifier = Modifier.size(18.dp).graphicsLayer { - if (pair.lastSyncResult == SyncStatus.SYNCING) rotationZ = syncRotation - }, - ) + when (pair.lastSyncResult) { + 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)) { + Icon(Icons.Default.PlayArrow, "Resume sync", modifier = Modifier.size(18.dp)) + } + else -> FilledTonalIconButton(onClick = onSync, modifier = Modifier.size(36.dp)) { + Icon(Icons.Default.Sync, "Sync now", modifier = Modifier.size(18.dp).graphicsLayer { rotationZ = syncRotation * 0f }) + } } } } @@ -189,6 +194,7 @@ private fun StatusPill(status: SyncStatus) { val (icon, label) = when (status) { SyncStatus.SUCCESS -> Pair(Icons.Default.CheckCircle, "Synced") SyncStatus.SYNCING -> Pair(Icons.Default.Sync, "Syncing…") + SyncStatus.PAUSED -> Pair(Icons.Default.Pause, "Paused") SyncStatus.FAILED -> Pair(Icons.Default.Error, "Failed") SyncStatus.CONFLICT -> Pair(Icons.Default.Warning, "Conflict") SyncStatus.PARTIAL -> Pair(Icons.Default.WarningAmber, "Partial") @@ -247,11 +253,12 @@ private fun EmptyState(modifier: Modifier = Modifier, onAdd: () -> Unit) { private val SyncStatus.accentColor: Color @Composable get() = when (this) { - SyncStatus.SUCCESS -> Color(0xFF2E7D32) // green — done, healthy - SyncStatus.SYNCING -> Color(0xFF1565C0) // blue — in progress - SyncStatus.FAILED -> Color(0xFFC62828) // red — error - SyncStatus.PARTIAL -> Color(0xFFE65100) // orange — some files failed - SyncStatus.CONFLICT -> Color(0xFFF9A825) // amber — needs resolution + SyncStatus.SUCCESS -> Color(0xFF2E7D32) + SyncStatus.SYNCING -> Color(0xFF1565C0) + SyncStatus.PAUSED -> Color(0xFF6A1B9A) + SyncStatus.FAILED -> Color(0xFFC62828) + SyncStatus.PARTIAL -> Color(0xFFE65100) + SyncStatus.CONFLICT -> Color(0xFFF9A825) SyncStatus.IDLE -> MaterialTheme.colorScheme.outline } diff --git a/app/src/main/kotlin/com/syncflow/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/syncflow/ui/home/HomeViewModel.kt index cf2776c..7dd822c 100644 --- a/app/src/main/kotlin/com/syncflow/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/syncflow/ui/home/HomeViewModel.kt @@ -8,6 +8,7 @@ import androidx.work.WorkManager import com.syncflow.data.db.SyncPairDao import com.syncflow.data.db.entities.SyncPairEntity import com.syncflow.domain.model.ScheduleType +import com.syncflow.domain.model.SyncStatus import com.syncflow.worker.FileWatchService import com.syncflow.worker.SyncWorker import dagger.hilt.android.lifecycle.HiltViewModel @@ -32,6 +33,11 @@ class HomeViewModel @Inject constructor( workManager.enqueue(req) } + fun pauseSync(pair: SyncPairEntity) { + workManager.cancelAllWorkByTag("sync_${pair.id}") + viewModelScope.launch { syncPairDao.updateStatus(pair.id, SyncStatus.PAUSED) } + } + fun toggleEnabled(pair: SyncPairEntity) { viewModelScope.launch { val nowEnabled = !pair.isEnabled diff --git a/app/src/main/kotlin/com/syncflow/ui/pairdetail/PairDetailScreen.kt b/app/src/main/kotlin/com/syncflow/ui/pairdetail/PairDetailScreen.kt index 9ac7a1c..6132570 100644 --- a/app/src/main/kotlin/com/syncflow/ui/pairdetail/PairDetailScreen.kt +++ b/app/src/main/kotlin/com/syncflow/ui/pairdetail/PairDetailScreen.kt @@ -66,7 +66,17 @@ fun PairDetailScreen( navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.Default.ArrowBack, "Back") } }, actions = { IconButton(onClick = { pair?.let { onEdit(it.id) } }) { Icon(Icons.Default.Edit, "Edit") } - IconButton(onClick = { vm.syncNow() }) { Icon(Icons.Default.Sync, "Sync now") } + when (pair?.lastSyncResult) { + SyncStatus.SYNCING -> IconButton(onClick = { vm.pauseSync() }) { + Icon(Icons.Default.Pause, "Pause sync") + } + SyncStatus.PAUSED -> IconButton(onClick = { vm.syncNow() }) { + Icon(Icons.Default.PlayArrow, "Resume sync") + } + else -> IconButton(onClick = { vm.syncNow() }) { + Icon(Icons.Default.Sync, "Sync now") + } + } IconButton(onClick = { showDelete = true }) { Icon(Icons.Default.Delete, "Delete") } }, ) @@ -142,6 +152,7 @@ private fun StatusBanner(pair: SyncPairEntity) { 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) + SyncStatus.PAUSED -> Triple(Icons.Default.Pause, "Paused — tap ▶ to resume", MaterialTheme.colorScheme.surfaceVariant) SyncStatus.FAILED -> Triple(Icons.Default.Error, "Failed", MaterialTheme.colorScheme.errorContainer) SyncStatus.CONFLICT -> Triple(Icons.Default.Warning, "Conflict", MaterialTheme.colorScheme.tertiaryContainer) SyncStatus.PARTIAL -> Triple(Icons.Default.WarningAmber,"Partial", MaterialTheme.colorScheme.tertiaryContainer) diff --git a/app/src/main/kotlin/com/syncflow/ui/pairdetail/PairDetailViewModel.kt b/app/src/main/kotlin/com/syncflow/ui/pairdetail/PairDetailViewModel.kt index 67a0594..917d796 100644 --- a/app/src/main/kotlin/com/syncflow/ui/pairdetail/PairDetailViewModel.kt +++ b/app/src/main/kotlin/com/syncflow/ui/pairdetail/PairDetailViewModel.kt @@ -7,6 +7,7 @@ import androidx.work.WorkManager import com.syncflow.data.db.SyncConflictDao import com.syncflow.data.db.SyncEventDao import com.syncflow.data.db.SyncPairDao +import com.syncflow.domain.model.SyncStatus import com.syncflow.worker.SyncWorker import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted @@ -39,6 +40,12 @@ class PairDetailViewModel @Inject constructor( workManager.enqueue(SyncWorker.buildOneTimeRequest(p.id, wifiOnly = false, chargingOnly = false)) } + fun pauseSync() { + val p = pair.value ?: return + workManager.cancelAllWorkByTag("sync_${p.id}") + viewModelScope.launch { syncPairDao.updateStatus(p.id, SyncStatus.PAUSED) } + } + fun delete() { viewModelScope.launch { pair.value?.let { syncPairDao.delete(it) } diff --git a/version.properties b/version.properties index 2ea449a..9027291 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=1.0.58 -VERSION_CODE=59 +VERSION_NAME=1.0.59 +VERSION_CODE=60