Merge branch 'claude/serene-mclean-b76496'

This commit is contained in:
2026-05-18 13:13:16 -04:00
2 changed files with 71 additions and 3 deletions

View File

@@ -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 -> {}
}
}
}
}

View File

@@ -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>(PingState.Idle)
val ping: StateFlow<PingState> = _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)
}
}
}