Compare commits

...

2 Commits

Author SHA1 Message Date
4597f92d93 merge: snackbar after sync 2026-05-18 21:04:38 -04:00
a37ef4a794 feat: snackbar after sync (synced N / nothing to sync / server unreachable)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:02:51 -04:00
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)
} }