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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.Text import androidx.compose.material3.Text
@@ -21,7 +26,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -31,6 +38,7 @@ import me.hgsky.synq.data.SynqSettings
@Composable @Composable
fun SettingsScreen(nav: NavController, vm: SettingsViewModel = viewModel()) { fun SettingsScreen(nav: NavController, vm: SettingsViewModel = viewModel()) {
val saved by vm.settings.collectAsState() val saved by vm.settings.collectAsState()
val ping by vm.ping.collectAsState()
var url by remember(saved.serverUrl) { mutableStateOf(saved.serverUrl) } var url by remember(saved.serverUrl) { mutableStateOf(saved.serverUrl) }
var token by remember(saved.token) { mutableStateOf(saved.token) } var token by remember(saved.token) { mutableStateOf(saved.token) }
@@ -81,10 +89,46 @@ fun SettingsScreen(nav: NavController, vm: SettingsViewModel = viewModel()) {
label = { Text("device label") }, label = { Text("device label") },
singleLine = true, singleLine = true,
) )
Button(
onClick = ::saveAndBack, Row(
modifier = Modifier.fillMaxWidth(), 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 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.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.hgsky.synq.SynqApp import me.hgsky.synq.SynqApp
import me.hgsky.synq.data.SynqSettings 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) { class SettingsViewModel(app: Application) : AndroidViewModel(app) {
@@ -19,7 +31,19 @@ class SettingsViewModel(app: Application) : AndroidViewModel(app) {
SynqSettings(), SynqSettings(),
) )
private val _ping = MutableStateFlow<PingState>(PingState.Idle)
val ping: StateFlow<PingState> = _ping.asStateFlow()
fun save(settings: SynqSettings) { fun save(settings: SynqSettings) {
viewModelScope.launch { repo.save(settings) } 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)
}
}
} }