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:
@@ -1,5 +1,6 @@
|
|||||||
package com.syncflow.ui.addpair
|
package com.syncflow.ui.addpair
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.SyncPairDao
|
||||||
import com.syncflow.data.db.entities.CloudAccountEntity
|
import com.syncflow.data.db.entities.CloudAccountEntity
|
||||||
import com.syncflow.data.db.entities.SyncPairEntity
|
import com.syncflow.data.db.entities.SyncPairEntity
|
||||||
import com.syncflow.data.db.entities.toDomain
|
|
||||||
import com.syncflow.domain.model.*
|
import com.syncflow.domain.model.*
|
||||||
|
import com.syncflow.worker.FileWatchService
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -31,7 +33,7 @@ data class AddPairUiState(
|
|||||||
val scheduleType: ScheduleType = ScheduleType.INTERVAL,
|
val scheduleType: ScheduleType = ScheduleType.INTERVAL,
|
||||||
val intervalMinutes: Int = 30,
|
val intervalMinutes: Int = 30,
|
||||||
val dailyTime: String = "02:00",
|
val dailyTime: String = "02:00",
|
||||||
val weekdays: Int = 0b1111111, // all 7 days by default
|
val weekdays: Int = 0b1111111,
|
||||||
// ── Constraints ──────────────────────────────────────────────────────────
|
// ── Constraints ──────────────────────────────────────────────────────────
|
||||||
val wifiOnly: Boolean = true,
|
val wifiOnly: Boolean = true,
|
||||||
val wifiSsid: String = "",
|
val wifiSsid: String = "",
|
||||||
@@ -57,6 +59,7 @@ data class AddPairUiState(
|
|||||||
class AddPairViewModel @Inject constructor(
|
class AddPairViewModel @Inject constructor(
|
||||||
private val syncPairDao: SyncPairDao,
|
private val syncPairDao: SyncPairDao,
|
||||||
private val accountDao: CloudAccountDao,
|
private val accountDao: CloudAccountDao,
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
savedState: SavedStateHandle,
|
savedState: SavedStateHandle,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -147,7 +150,10 @@ class AddPairViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
if (editPairId == null) syncPairDao.insert(entity) else syncPairDao.update(entity)
|
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) } }
|
.onFailure { e -> _state.update { it.copy(isSaving = false, error = e.message) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.syncflow.ui.home
|
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.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -14,6 +19,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.syncflow.data.db.entities.SyncPairEntity
|
import com.syncflow.data.db.entities.SyncPairEntity
|
||||||
@@ -159,8 +165,18 @@ private fun SyncPairCard(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
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)) {
|
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")
|
SyncStatus.IDLE -> Pair(Icons.Outlined.Circle, "Idle")
|
||||||
}
|
}
|
||||||
val containerColor = status.accentColor
|
val containerColor = status.accentColor
|
||||||
val contentColor = when (status) {
|
val rotation by rememberInfiniteTransition(label = "syncSpin").animateFloat(
|
||||||
SyncStatus.IDLE -> MaterialTheme.colorScheme.onSurfaceVariant
|
initialValue = 0f, targetValue = 360f,
|
||||||
else -> MaterialTheme.colorScheme.surface
|
animationSpec = infiniteRepeatable(tween(1000, easing = LinearEasing)),
|
||||||
}
|
label = "rotation",
|
||||||
|
)
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(50),
|
shape = RoundedCornerShape(50),
|
||||||
color = containerColor.copy(alpha = 0.15f),
|
color = containerColor.copy(alpha = 0.15f),
|
||||||
@@ -192,7 +209,11 @@ private fun StatusPill(status: SyncStatus) {
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
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)
|
Text(label, style = MaterialTheme.typography.labelSmall, color = containerColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.syncflow.ui.pairdetail
|
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.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -13,6 +18,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.syncflow.data.db.entities.SyncEventEntity
|
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.PARTIAL -> Triple(Icons.Default.WarningAmber,"Partial", MaterialTheme.colorScheme.tertiaryContainer)
|
||||||
SyncStatus.IDLE -> Triple(Icons.Outlined.Circle, "Idle", MaterialTheme.colorScheme.surfaceVariant)
|
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(
|
Surface(
|
||||||
color = containerColor,
|
color = containerColor,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
@@ -152,7 +163,12 @@ private fun StatusBanner(pair: SyncPairEntity) {
|
|||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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))
|
Spacer(Modifier.width(16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(label, style = MaterialTheme.typography.titleMedium)
|
Text(label, style = MaterialTheme.typography.titleMedium)
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class FileWatchService : Service() {
|
|||||||
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
if (nm.getNotificationChannel(CHANNEL_WATCH) == null) {
|
if (nm.getNotificationChannel(CHANNEL_WATCH) == null) {
|
||||||
nm.createNotificationChannel(
|
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"
|
description = "Background service watching folders for changes"
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<gradient
|
<gradient
|
||||||
android:type="linear"
|
android:type="linear"
|
||||||
android:angle="135"
|
android:angle="135"
|
||||||
android:startColor="#312E81"
|
android:startColor="#4338CA"
|
||||||
android:endColor="#6366F1"/>
|
android:centerColor="#7C3AED"
|
||||||
|
android:endColor="#0891B2"/>
|
||||||
</shape>
|
</shape>
|
||||||
|
|||||||
@@ -1,13 +1,39 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<!-- Soft white glow ring behind arrows -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:pathData="M12,12m-7.5,0a7.5,7.5 0 1,0 15,0a7.5,7.5 0 1,0 -15,0"
|
||||||
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:fillColor="#22FFFFFF"/>
|
||||||
|
|
||||||
|
<!-- Upload arrow — white → sky blue -->
|
||||||
<path
|
<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,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="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>
|
</vector>
|
||||||
|
|||||||
Binary file not shown.
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
VERSION_NAME=1.0.15
|
VERSION_NAME=1.0.16
|
||||||
VERSION_CODE=16
|
VERSION_CODE=17
|
||||||
|
|||||||
Reference in New Issue
Block a user