v1.0.16: spinning sync icon, colorful icon, ON_CHANGE fix, notification fix

- Sync icon now rotates (CSS-style spin) in StatusPill, StatusBanner,
  and card sync button whenever status is SYNCING
- Launcher icon redesigned: indigo→violet→cyan gradient background,
  upload arrow fades white→sky-blue, download arrow fades white→violet,
  soft glow ring behind arrows
- Fix ON_CHANGE not triggering: FileWatchService.start() now called
  from AddPairViewModel.save() so pairs created with ON_CHANGE
  immediately begin watching without needing a toggle or reboot
- Fix FileWatch notification hidden: IMPORTANCE_MIN → IMPORTANCE_LOW
  so the "Watching N folders" notification shows in the shade

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 03:42:30 +00:00
parent bcfecbb867
commit b15637132c
9 changed files with 89 additions and 19 deletions
@@ -1,5 +1,6 @@
package com.syncflow.ui.addpair
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -7,9 +8,10 @@ import com.syncflow.data.db.CloudAccountDao
import com.syncflow.data.db.SyncPairDao
import com.syncflow.data.db.entities.CloudAccountEntity
import com.syncflow.data.db.entities.SyncPairEntity
import com.syncflow.data.db.entities.toDomain
import com.syncflow.domain.model.*
import com.syncflow.worker.FileWatchService
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -31,7 +33,7 @@ data class AddPairUiState(
val scheduleType: ScheduleType = ScheduleType.INTERVAL,
val intervalMinutes: Int = 30,
val dailyTime: String = "02:00",
val weekdays: Int = 0b1111111, // all 7 days by default
val weekdays: Int = 0b1111111,
// ── Constraints ──────────────────────────────────────────────────────────
val wifiOnly: Boolean = true,
val wifiSsid: String = "",
@@ -57,6 +59,7 @@ data class AddPairUiState(
class AddPairViewModel @Inject constructor(
private val syncPairDao: SyncPairDao,
private val accountDao: CloudAccountDao,
@ApplicationContext private val context: Context,
savedState: SavedStateHandle,
) : ViewModel() {
@@ -147,7 +150,10 @@ class AddPairViewModel @Inject constructor(
)
if (editPairId == null) syncPairDao.insert(entity) else syncPairDao.update(entity)
}
.onSuccess { _state.update { it.copy(done = true) } }
.onSuccess {
if (s.scheduleType == ScheduleType.ON_CHANGE) FileWatchService.start(context)
_state.update { it.copy(done = true) }
}
.onFailure { e -> _state.update { it.copy(isSaving = false, error = e.message) } }
}
}
@@ -1,5 +1,10 @@
package com.syncflow.ui.home
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -14,6 +19,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.syncflow.data.db.entities.SyncPairEntity
@@ -159,8 +165,18 @@ private fun SyncPairCard(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
val syncRotation by rememberInfiniteTransition(label = "cardSyncSpin").animateFloat(
initialValue = 0f, targetValue = 360f,
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))
Icon(
Icons.Default.Sync, "Sync now",
modifier = Modifier.size(18.dp).graphicsLayer {
if (pair.lastSyncResult == SyncStatus.SYNCING) rotationZ = syncRotation
},
)
}
}
}
@@ -179,10 +195,11 @@ private fun StatusPill(status: SyncStatus) {
SyncStatus.IDLE -> Pair(Icons.Outlined.Circle, "Idle")
}
val containerColor = status.accentColor
val contentColor = when (status) {
SyncStatus.IDLE -> MaterialTheme.colorScheme.onSurfaceVariant
else -> MaterialTheme.colorScheme.surface
}
val rotation by rememberInfiniteTransition(label = "syncSpin").animateFloat(
initialValue = 0f, targetValue = 360f,
animationSpec = infiniteRepeatable(tween(1000, easing = LinearEasing)),
label = "rotation",
)
Surface(
shape = RoundedCornerShape(50),
color = containerColor.copy(alpha = 0.15f),
@@ -192,7 +209,11 @@ private fun StatusPill(status: SyncStatus) {
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(icon, null, Modifier.size(12.dp), tint = containerColor)
Icon(
icon, null,
Modifier.size(12.dp).graphicsLayer { if (status == SyncStatus.SYNCING) rotationZ = rotation },
tint = containerColor,
)
Text(label, style = MaterialTheme.typography.labelSmall, color = containerColor)
}
}
@@ -1,5 +1,10 @@
package com.syncflow.ui.pairdetail
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -13,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.syncflow.data.db.entities.SyncEventEntity
@@ -143,6 +149,11 @@ private fun StatusBanner(pair: SyncPairEntity) {
SyncStatus.PARTIAL -> Triple(Icons.Default.WarningAmber,"Partial", MaterialTheme.colorScheme.tertiaryContainer)
SyncStatus.IDLE -> Triple(Icons.Outlined.Circle, "Idle", MaterialTheme.colorScheme.surfaceVariant)
}
val rotation by rememberInfiniteTransition(label = "bannerSpin").animateFloat(
initialValue = 0f, targetValue = 360f,
animationSpec = infiniteRepeatable(tween(1000, easing = LinearEasing)),
label = "bannerRotation",
)
Surface(
color = containerColor,
shape = RoundedCornerShape(16.dp),
@@ -152,7 +163,12 @@ private fun StatusBanner(pair: SyncPairEntity) {
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(icon, null, modifier = Modifier.size(40.dp))
Icon(
icon, null,
modifier = Modifier.size(40.dp).graphicsLayer {
if (pair.lastSyncResult == SyncStatus.SYNCING) rotationZ = rotation
},
)
Spacer(Modifier.width(16.dp))
Column {
Text(label, style = MaterialTheme.typography.titleMedium)
@@ -148,7 +148,7 @@ class FileWatchService : Service() {
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (nm.getNotificationChannel(CHANNEL_WATCH) == null) {
nm.createNotificationChannel(
NotificationChannel(CHANNEL_WATCH, "File watching", NotificationManager.IMPORTANCE_MIN).apply {
NotificationChannel(CHANNEL_WATCH, "File watching", NotificationManager.IMPORTANCE_LOW).apply {
description = "Background service watching folders for changes"
setShowBadge(false)
}
@@ -3,6 +3,7 @@
<gradient
android:type="linear"
android:angle="135"
android:startColor="#312E81"
android:endColor="#6366F1"/>
android:startColor="#4338CA"
android:centerColor="#7C3AED"
android:endColor="#0891B2"/>
</shape>
@@ -1,13 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Soft white glow ring behind arrows -->
<path
android:fillColor="#FFFFFF"
android:pathData="M12,4V1L8,5l4,4V6c3.31,0 6,2.69 6,6 0,1.01-0.25,1.97-0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0-4.42-3.58-8-8-8z"/>
android:pathData="M12,12m-7.5,0a7.5,7.5 0 1,0 15,0a7.5,7.5 0 1,0 -15,0"
android:fillColor="#22FFFFFF"/>
<!-- Upload arrow — white → sky blue -->
<path
android:fillColor="#FFFFFF"
android:pathData="M12,18c-3.31,0-6,-2.69-6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4-4,-4v3z"/>
android:pathData="M12,4V1L8,5l4,4V6c3.31,0 6,2.69 6,6 0,1.01-0.25,1.97-0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0-4.42-3.58-8-8-8z">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="8" android:startY="1"
android:endX="20" android:endY="15"
android:startColor="#FFFFFF"
android:endColor="#7DD3FC"/>
</aapt:attr>
</path>
<!-- Download arrow — white → violet -->
<path
android:pathData="M12,18c-3.31,0-6,-2.69-6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4-4,-4v3z">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="4" android:startY="8"
android:endX="16" android:endY="23"
android:startColor="#FFFFFF"
android:endColor="#C4B5FD"/>
</aapt:attr>
</path>
</vector>
Binary file not shown.
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME=1.0.15
VERSION_CODE=16
VERSION_NAME=1.0.16
VERSION_CODE=17