From a37ef4a794fd95efb9a9dcfa7652c3c7b2738b56 Mon Sep 17 00:00:00 2001 From: eulaly Date: Mon, 18 May 2026 21:02:51 -0400 Subject: [PATCH] feat: snackbar after sync (synced N / nothing to sync / server unreachable) Co-Authored-By: Claude Sonnet 4.6 --- .../kotlin/me/hgsky/synq/data/sync/SyncService.kt | 5 ++++- .../kotlin/me/hgsky/synq/ui/capture/CaptureScreen.kt | 7 +++++++ .../me/hgsky/synq/ui/capture/CaptureViewModel.kt | 12 ++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/kotlin/me/hgsky/synq/data/sync/SyncService.kt b/android/app/src/main/kotlin/me/hgsky/synq/data/sync/SyncService.kt index 4f24d20..22bcf6a 100644 --- a/android/app/src/main/kotlin/me/hgsky/synq/data/sync/SyncService.kt +++ b/android/app/src/main/kotlin/me/hgsky/synq/data/sync/SyncService.kt @@ -7,8 +7,9 @@ import me.hgsky.synq.data.db.CaptureDao import java.time.OffsetDateTime import java.time.format.DateTimeFormatter -suspend fun syncPending(dao: CaptureDao, client: SynqApiClient, settings: SynqSettings) { +suspend fun syncPending(dao: CaptureDao, client: SynqApiClient, settings: SynqSettings): Int { val pending = dao.getPendingAndFailed() + var synced = 0 for (capture in pending) { dao.updateStatus(capture.id, "syncing", null) val result = client.postCapture(capture, settings) @@ -16,8 +17,10 @@ suspend fun syncPending(dao: CaptureDao, client: SynqApiClient, settings: SynqSe is PostResult.Accepted, is PostResult.AlreadySeen -> { val now = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) dao.markSynced(capture.id, now) + synced++ } is PostResult.Failed -> dao.updateStatus(capture.id, "failed", result.error) } } + return synced } diff --git a/android/app/src/main/kotlin/me/hgsky/synq/ui/capture/CaptureScreen.kt b/android/app/src/main/kotlin/me/hgsky/synq/ui/capture/CaptureScreen.kt index 1c2721e..d4119aa 100644 --- a/android/app/src/main/kotlin/me/hgsky/synq/ui/capture/CaptureScreen.kt +++ b/android/app/src/main/kotlin/me/hgsky/synq/ui/capture/CaptureScreen.kt @@ -30,6 +30,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -62,9 +64,13 @@ fun CaptureScreen( val recentTags by vm.recentTags.collectAsState() val lastSyncedAt by vm.lastSyncedAt.collectAsState() val focusRequester = remember { FocusRequester() } + val snackbarState = remember { SnackbarHostState() } val context = LocalContext.current LaunchedEffect(Unit) { focusRequester.requestFocus() } + LaunchedEffect(Unit) { + vm.snackbar.collect { msg -> snackbarState.showSnackbar(msg) } + } LaunchedEffect(prefill) { if (!prefill.isNullOrEmpty()) vm.setBody(prefill) } @@ -83,6 +89,7 @@ fun CaptureScreen( } Scaffold( + snackbarHost = { SnackbarHost(snackbarState) }, topBar = { TopAppBar( title = { Text("synq") }, diff --git a/android/app/src/main/kotlin/me/hgsky/synq/ui/capture/CaptureViewModel.kt b/android/app/src/main/kotlin/me/hgsky/synq/ui/capture/CaptureViewModel.kt index d8edcc3..600f0bc 100644 --- a/android/app/src/main/kotlin/me/hgsky/synq/ui/capture/CaptureViewModel.kt +++ b/android/app/src/main/kotlin/me/hgsky/synq/ui/capture/CaptureViewModel.kt @@ -4,9 +4,11 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -37,6 +39,9 @@ class CaptureViewModel(app: Application) : AndroidViewModel(app) { private val _state = MutableStateFlow(CaptureUiState()) val state: StateFlow = _state.asStateFlow() + private val _snackbar = MutableSharedFlow(extraBufferCapacity = 1) + val snackbar = _snackbar.asSharedFlow() + val recentTags: StateFlow> = dao.getRecentTagsJson() .map { jsonList -> val seen = LinkedHashSet() @@ -93,9 +98,12 @@ class CaptureViewModel(app: Application) : AndroidViewModel(app) { try { val settings = synqApp.settings.settings.first() val client = SynqApiClient() - if (client.checkHealth(settings.serverUrl)) { - syncPending(dao, client, settings) + if (!client.checkHealth(settings.serverUrl)) { + _snackbar.tryEmit("server unreachable") + return@launch } + val synced = syncPending(dao, client, settings) + _snackbar.tryEmit(if (synced == 0) "nothing to sync" else "synced $synced") } finally { _state.value = _state.value.copy(isSyncing = false) }