From ff5503e30c22ae709778af2d75594e316c553d45 Mon Sep 17 00:00:00 2001 From: eulaly Date: Mon, 18 May 2026 13:13:16 -0400 Subject: [PATCH] add health check button to settings screen Co-Authored-By: Claude Sonnet 4.6 --- .../hgsky/synq/ui/settings/SettingsScreen.kt | 50 +++++++++++++++++-- .../synq/ui/settings/SettingsViewModel.kt | 24 +++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/kotlin/me/hgsky/synq/ui/settings/SettingsScreen.kt b/android/app/src/main/kotlin/me/hgsky/synq/ui/settings/SettingsScreen.kt index 31448cf..32c3f3f 100644 --- a/android/app/src/main/kotlin/me/hgsky/synq/ui/settings/SettingsScreen.kt +++ b/android/app/src/main/kotlin/me/hgsky/synq/ui/settings/SettingsScreen.kt @@ -2,15 +2,20 @@ package me.hgsky.synq.ui.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -21,7 +26,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController @@ -31,6 +38,7 @@ import me.hgsky.synq.data.SynqSettings @Composable fun SettingsScreen(nav: NavController, vm: SettingsViewModel = viewModel()) { val saved by vm.settings.collectAsState() + val ping by vm.ping.collectAsState() var url by remember(saved.serverUrl) { mutableStateOf(saved.serverUrl) } var token by remember(saved.token) { mutableStateOf(saved.token) } @@ -81,10 +89,46 @@ fun SettingsScreen(nav: NavController, vm: SettingsViewModel = viewModel()) { label = { Text("device label") }, singleLine = true, ) - Button( - onClick = ::saveAndBack, + + Row( modifier = Modifier.fillMaxWidth(), - ) { Text("save") } + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedButton( + onClick = { + vm.save(SynqSettings(url.trim(), token.trim(), device.trim().ifEmpty { "android" })) + vm.checkConnection(url.trim()) + }, + modifier = Modifier.weight(1f), + enabled = ping !is PingState.Checking, + ) { + if (ping is PingState.Checking) { + CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp) + } else { + Text("check connection") + } + } + + Button( + onClick = ::saveAndBack, + modifier = Modifier.weight(1f), + ) { Text("save") } + } + + when (val p = ping) { + is PingState.Ok -> Text( + "✓ reachable", + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF2E7D32), + ) + is PingState.Failed -> Text( + "✗ unreachable — check URL and that server is running", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + else -> {} + } } } } diff --git a/android/app/src/main/kotlin/me/hgsky/synq/ui/settings/SettingsViewModel.kt b/android/app/src/main/kotlin/me/hgsky/synq/ui/settings/SettingsViewModel.kt index 6d045e4..d699947 100644 --- a/android/app/src/main/kotlin/me/hgsky/synq/ui/settings/SettingsViewModel.kt +++ b/android/app/src/main/kotlin/me/hgsky/synq/ui/settings/SettingsViewModel.kt @@ -3,11 +3,23 @@ package me.hgsky.synq.ui.settings import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import me.hgsky.synq.SynqApp import me.hgsky.synq.data.SynqSettings +import me.hgsky.synq.data.api.SynqApiClient + +sealed class PingState { + object Idle : PingState() + object Checking : PingState() + data class Ok(val url: String) : PingState() + data class Failed(val url: String) : PingState() +} class SettingsViewModel(app: Application) : AndroidViewModel(app) { @@ -19,7 +31,19 @@ class SettingsViewModel(app: Application) : AndroidViewModel(app) { SynqSettings(), ) + private val _ping = MutableStateFlow(PingState.Idle) + val ping: StateFlow = _ping.asStateFlow() + fun save(settings: SynqSettings) { viewModelScope.launch { repo.save(settings) } } + + fun checkConnection(url: String) { + if (_ping.value is PingState.Checking) return + viewModelScope.launch(Dispatchers.IO) { + _ping.value = PingState.Checking + val ok = SynqApiClient().checkHealth(url.trim()) + _ping.value = if (ok) PingState.Ok(url) else PingState.Failed(url) + } + } }