Merge branch 'claude/serene-mclean-b76496'
This commit is contained in:
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
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(
|
Button(
|
||||||
onClick = ::saveAndBack,
|
onClick = ::saveAndBack,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.weight(1f),
|
||||||
) { Text("save") }
|
) { 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 -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user