Compare commits

...

4 Commits

Author SHA1 Message Date
amir 894c2ffe78 v1.0.18: fix ON_CHANGE never starting, improve version display
- Start FileWatchService from SyncFlowApp.onCreate() for any existing
  enabled ON_CHANGE pairs — previously the watcher only started on
  device boot or explicit pair toggle, so existing pairs after an
  app update never got watched
- About screen now shows "Version X.Y.Z (build N)" updating
  automatically from BuildConfig on every release

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 10:11:47 +00:00
amir 59335dab13 v1.0.17: modern multi-color app icon with depth and detail
Redesigned launcher icon:
- Background: deep violet #2E1065 → purple #6D28D9 → navy #1E40AF
- Three concentric glow rings (white, layered alpha) for depth
- Upload arrow: neon cyan #67E8F9 → sky blue #38BDF8
- Download arrow: hot pink #F472B6 → coral #FB923C
- Double-layer center orb (frosted + solid white)
- 4 cardinal accent sparks (cyan/indigo/pink/emerald)
- 4 diagonal mini sparks (light cyan/peach/violet/green)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 03:48:18 +00:00
amir b15637132c 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>
2026-05-24 03:42:30 +00:00
amir bcfecbb867 releases/latest: add v1.0.15 APK
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 02:55:48 +00:00
11 changed files with 143 additions and 24 deletions
@@ -3,7 +3,13 @@ package com.syncflow
import android.app.Application import android.app.Application
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration import androidx.work.Configuration
import com.syncflow.data.db.SyncPairDao
import com.syncflow.domain.model.ScheduleType
import com.syncflow.worker.FileWatchService
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@@ -11,10 +17,16 @@ import javax.inject.Inject
class SyncFlowApp : Application(), Configuration.Provider { class SyncFlowApp : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory @Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var syncPairDao: SyncPairDao
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
// Start file watcher on every app launch for any existing ON_CHANGE pairs
CoroutineScope(Dispatchers.IO).launch {
val hasOnChange = syncPairDao.getEnabled().any { it.scheduleType == ScheduleType.ON_CHANGE }
if (hasOnChange) FileWatchService.start(this@SyncFlowApp)
}
} }
override val workManagerConfiguration: Configuration override val workManagerConfiguration: Configuration
@@ -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)
@@ -137,11 +137,17 @@ fun SettingsScreen(
) { ) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text( Text(
"SyncFlow v${com.syncflow.BuildConfig.VERSION_NAME} — Free, no subscription.", "SyncFlow",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.titleSmall,
) )
Text( Text(
"Open source. No ads. No tracking.", "Version ${com.syncflow.BuildConfig.VERSION_NAME} (build ${com.syncflow.BuildConfig.VERSION_CODE})",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(2.dp))
Text(
"Free, no subscription. No ads. No tracking.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -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="#2E1065"
android:endColor="#6366F1"/> android:centerColor="#6D28D9"
android:endColor="#1E40AF"/>
</shape> </shape>
@@ -1,13 +1,70 @@
<?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="108"
android:viewportHeight="24"> android:viewportHeight="108">
<!-- Outer soft glow ring -->
<path <path
android:fillColor="#FFFFFF" android:pathData="M54,54m-44,0a44,44 0 1,0 88,0a44,44 0 1,0 -88,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="#12FFFFFF"/>
<!-- Mid glow ring -->
<path <path
android:fillColor="#FFFFFF" android:pathData="M54,54m-33,0a33,33 0 1,0 66,0a33,33 0 1,0 -66,0"
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:fillColor="#18FFFFFF"/>
<!-- Inner glow ring -->
<path
android:pathData="M54,54m-21,0a21,21 0 1,0 42,0a21,21 0 1,0 -42,0"
android:fillColor="#10FFFFFF"/>
<!-- Upload arrow (top-right) — neon cyan → sky blue -->
<path android:pathData="M54,18V4.5L36,22.5l18,18V27c14.895,0 27,12.105 27,27 0,4.545-1.125,8.865-3.15,12.6l6.57,6.57C87.93,67.635 90,61.065 90,54c0-19.89-16.11-36-36-36z">
<aapt:attr name="android:fillColor">
<gradient android:type="linear"
android:startX="36" android:startY="4"
android:endX="90" android:endY="70"
android:startColor="#67E8F9"
android:endColor="#38BDF8"/>
</aapt:attr>
</path>
<!-- Download arrow (bottom-left) — hot pink → coral -->
<path android:pathData="M54,81c-14.895,0-27,-12.105-27,-27 0,-4.545 1.125,-8.865 3.15,-12.6L23.58,34.83C20.07,40.365 18,46.935 18,54c0,19.89 16.11,36 36,36v13.5l18,-18-18,-18v13.5z">
<aapt:attr name="android:fillColor">
<gradient android:type="linear"
android:startX="18" android:startY="35"
android:endX="72" android:endY="103"
android:startColor="#F472B6"
android:endColor="#FB923C"/>
</aapt:attr>
</path>
<!-- Center glowing orb -->
<path
android:pathData="M54,54m-7,0a7,7 0 1,0 14,0a7,7 0 1,0 -14,0"
android:fillColor="#60FFFFFF"/>
<path
android:pathData="M54,54m-4,0a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"
android:fillColor="#FFFFFF"/>
<!-- Cardinal accent sparks -->
<!-- Top — cyan -->
<path android:pathData="M54,13m-3,0a3,3 0 1,0 6,0a3,3 0 1,0 -6,0" android:fillColor="#22D3EE"/>
<!-- Right — indigo -->
<path android:pathData="M95,54m-3,0a3,3 0 1,0 6,0a3,3 0 1,0 -6,0" android:fillColor="#818CF8"/>
<!-- Bottom — pink -->
<path android:pathData="M54,95m-3,0a3,3 0 1,0 6,0a3,3 0 1,0 -6,0" android:fillColor="#F9A8D4"/>
<!-- Left — emerald -->
<path android:pathData="M13,54m-3,0a3,3 0 1,0 6,0a3,3 0 1,0 -6,0" android:fillColor="#6EE7B7"/>
<!-- Diagonal mini sparks (45°) -->
<path android:pathData="M85,23m-2,0a2,2 0 1,0 4,0a2,2 0 1,0 -4,0" android:fillColor="#A5F3FC"/>
<path android:pathData="M85,85m-2,0a2,2 0 1,0 4,0a2,2 0 1,0 -4,0" android:fillColor="#FDBA74"/>
<path android:pathData="M23,85m-2,0a2,2 0 1,0 4,0a2,2 0 1,0 -4,0" android:fillColor="#C084FC"/>
<path android:pathData="M23,23m-2,0a2,2 0 1,0 4,0a2,2 0 1,0 -4,0" android:fillColor="#86EFAC"/>
</vector> </vector>
Binary file not shown.
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME=1.0.15 VERSION_NAME=1.0.18
VERSION_CODE=16 VERSION_CODE=19