feat: snackbar after sync (synced N / nothing to sync / server unreachable)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 21:02:51 -04:00
parent 415742b974
commit a37ef4a794
3 changed files with 21 additions and 3 deletions

View File

@@ -7,8 +7,9 @@ import me.hgsky.synq.data.db.CaptureDao
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter 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() val pending = dao.getPendingAndFailed()
var synced = 0
for (capture in pending) { for (capture in pending) {
dao.updateStatus(capture.id, "syncing", null) dao.updateStatus(capture.id, "syncing", null)
val result = client.postCapture(capture, settings) 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 -> { is PostResult.Accepted, is PostResult.AlreadySeen -> {
val now = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) val now = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
dao.markSynced(capture.id, now) dao.markSynced(capture.id, now)
synced++
} }
is PostResult.Failed -> dao.updateStatus(capture.id, "failed", result.error) is PostResult.Failed -> dao.updateStatus(capture.id, "failed", result.error)
} }
} }
return synced
} }

View File

@@ -30,6 +30,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -62,9 +64,13 @@ fun CaptureScreen(
val recentTags by vm.recentTags.collectAsState() val recentTags by vm.recentTags.collectAsState()
val lastSyncedAt by vm.lastSyncedAt.collectAsState() val lastSyncedAt by vm.lastSyncedAt.collectAsState()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val snackbarState = remember { SnackbarHostState() }
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { focusRequester.requestFocus() } LaunchedEffect(Unit) { focusRequester.requestFocus() }
LaunchedEffect(Unit) {
vm.snackbar.collect { msg -> snackbarState.showSnackbar(msg) }
}
LaunchedEffect(prefill) { LaunchedEffect(prefill) {
if (!prefill.isNullOrEmpty()) vm.setBody(prefill) if (!prefill.isNullOrEmpty()) vm.setBody(prefill)
} }
@@ -83,6 +89,7 @@ fun CaptureScreen(
} }
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(snackbarState) },
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text("synq") }, title = { Text("synq") },

View File

@@ -4,9 +4,11 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -37,6 +39,9 @@ class CaptureViewModel(app: Application) : AndroidViewModel(app) {
private val _state = MutableStateFlow(CaptureUiState()) private val _state = MutableStateFlow(CaptureUiState())
val state: StateFlow<CaptureUiState> = _state.asStateFlow() val state: StateFlow<CaptureUiState> = _state.asStateFlow()
private val _snackbar = MutableSharedFlow<String>(extraBufferCapacity = 1)
val snackbar = _snackbar.asSharedFlow()
val recentTags: StateFlow<List<String>> = dao.getRecentTagsJson() val recentTags: StateFlow<List<String>> = dao.getRecentTagsJson()
.map { jsonList -> .map { jsonList ->
val seen = LinkedHashSet<String>() val seen = LinkedHashSet<String>()
@@ -93,9 +98,12 @@ class CaptureViewModel(app: Application) : AndroidViewModel(app) {
try { try {
val settings = synqApp.settings.settings.first() val settings = synqApp.settings.settings.first()
val client = SynqApiClient() val client = SynqApiClient()
if (client.checkHealth(settings.serverUrl)) { if (!client.checkHealth(settings.serverUrl)) {
syncPending(dao, client, settings) _snackbar.tryEmit("server unreachable")
return@launch
} }
val synced = syncPending(dao, client, settings)
_snackbar.tryEmit(if (synced == 0) "nothing to sync" else "synced $synced")
} finally { } finally {
_state.value = _state.value.copy(isSyncing = false) _state.value = _state.value.copy(isSyncing = false)
} }