Redesign server config

This commit is contained in:
2025-12-22 16:09:27 +03:00
Unverified
parent 4fe6903deb
commit d28fadc352
7 changed files with 164 additions and 159 deletions
@@ -67,4 +67,6 @@
<string name="typing_single">%1$s is typing…</string>
<string name="typing_two">%1$s and %2$s are typing…</string>
<string name="typing_many">%1$s, %2$s and %3$d more are typing…</string>
<string name="more">More</string>
</resources>
@@ -21,6 +21,9 @@ object Config {
*/
suspend fun initialize() {
_serverConfig.value = ServerConfigStorage.getConfig()
// Set default values and prevent corruption
ServerConfigStorage.getConfig()
}
/**
@@ -3,19 +3,11 @@ package ru.fromchat.ui
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.End
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Start
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
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.unit.IntOffset
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -25,8 +17,6 @@ import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import ru.fromchat.api.WebSocketManager
import ru.fromchat.core.config.Config
import ru.fromchat.ui.auth.LoginScreen
@@ -39,30 +29,9 @@ val LocalNavController = compositionLocalOf<NavController> { error("") }
@Composable
fun App() {
var startDestination by remember { mutableStateOf<String?>(null) }
// Initialize config and check server configuration on startup
LaunchedEffect(Unit) {
coroutineScope {
launch {
try {
// Initialize config
Config.initialize()
// Check if server is configured
val serverConfigured = Config.hasServerConfig()
// Determine which screen to show
startDestination = if (!serverConfigured) {
"serverConfig"
} else {
"login"
}
} catch (e: Exception) {
// On error, start with server config
startDestination = "serverConfig"
}
}
runCatching {
Config.initialize()
}
}
@@ -96,82 +65,73 @@ fun App() {
CompositionLocalProvider(
LocalNavController provides navController
) {
if (startDestination == null) {
Box(
modifier = androidx.compose.ui.Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
NavHost(
navController = navController,
startDestination = startDestination!!,
enterTransition = {
slideIntoContainer(
Start,
animationSpec = animationSpec
)
},
exitTransition = {
slideOutOfContainer(
Start,
animationSpec = animationSpec
)
},
popEnterTransition = {
slideIntoContainer(
End,
animationSpec = animationSpec
)
},
popExitTransition = {
slideOutOfContainer(
End,
animationSpec = animationSpec
)
}
) {
composable("serverConfig") {
ServerConfigScreen()
}
composable("login") {
LoginScreen(
onLoginSuccess = {
navController.navigate("chat") {
popUpTo("login") { inclusive = true }
}
},
onNavigateToRegister = { navController.navigate("register") }
)
}
composable("register") {
RegisterScreen(
onRegistered = { navController.navigate("login") }
)
}
composable("chat") {
MainScreen(
onLogout = {
navController.navigate("login") {
popUpTo("chat") { inclusive = true }
}
}
)
}
composable("chats/publicChat") {
PublicChatScreen()
}
composable("about") {
AboutScreen()
}
NavHost(
navController = navController,
startDestination = "login",
enterTransition = {
slideIntoContainer(
Start,
animationSpec = animationSpec
)
},
exitTransition = {
slideOutOfContainer(
Start,
animationSpec = animationSpec
)
},
popEnterTransition = {
slideIntoContainer(
End,
animationSpec = animationSpec
)
},
popExitTransition = {
slideOutOfContainer(
End,
animationSpec = animationSpec
)
}
}
) {
composable("serverConfig") {
ServerConfigScreen()
}
composable("login") {
LoginScreen(
onLoginSuccess = {
navController.navigate("chat") {
popUpTo("login") { inclusive = true }
}
},
onNavigateToRegister = { navController.navigate("register") }
)
}
composable("register") {
RegisterScreen(
onRegistered = { navController.navigate("login") }
)
}
composable("chat") {
MainScreen(
onLogout = {
navController.navigate("login") {
popUpTo("chat") { inclusive = true }
}
}
)
}
composable("chats/publicChat") {
PublicChatScreen()
}
composable("about") {
AboutScreen()
}
}
}
}
}
@@ -1,21 +1,31 @@
package ru.fromchat.ui.auth
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Login
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -33,24 +43,73 @@ import ru.fromchat.Res
import ru.fromchat.api.ApiClient
import ru.fromchat.api.LoginRequest
import ru.fromchat.api.apiRequest
import ru.fromchat.change_server
import ru.fromchat.error_unexpected
import ru.fromchat.fill_all_fields
import ru.fromchat.login
import ru.fromchat.login_d
import ru.fromchat.more
import ru.fromchat.password
import ru.fromchat.register_button
import ru.fromchat.ui.LocalNavController
import ru.fromchat.ui.RowHeader
import ru.fromchat.username
import ru.fromchat.welcome
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
onLoginSuccess: () -> Unit,
onNavigateToRegister: () -> Unit
) {
val errorUnexpected = stringResource(Res.string.error_unexpected)
val navController = LocalNavController.current
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Scaffold(
contentWindowInsets = WindowInsets.safeDrawing,
topBar = {
TopAppBar(
actions = {
var expanded by remember { mutableStateOf(false) }
Box(Modifier.wrapContentSize(Alignment.TopEnd)) {
IconButton(
onClick = {
expanded = true
}
) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(Res.string.more)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false } // Закрыть при нажатии вне меню
) {
DropdownMenuItem(
text = {
Text(stringResource(Res.string.change_server))
},
onClick = {
expanded = false
navController.navigate("serverConfig")
},
leadingIcon = {
Icon(
Icons.Filled.Storage,
contentDescription = null
)
}
)
}
}
},
title = {}
)
}
) { innerPadding ->
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var alert by remember { mutableStateOf<String?>(null) }
@@ -188,20 +188,6 @@ fun SettingsTab(
onClick = {
// Logout and navigate to server config when server changes
scope.launch {
// Logout from current server
try {
ApiClient.token?.let { token ->
ApiClient.logout(token)
}
} catch (e: Exception) {
// Ignore logout errors (server might be unreachable)
}
// Clear API client state
ApiClient.token = null
ApiClient.user = null
WebSocketManager.shutdown()
navController.navigate("serverConfig")
}
},
@@ -52,7 +52,6 @@ import ru.fromchat.https_enabled
import ru.fromchat.save_continue
import ru.fromchat.server_config_subtitle
import ru.fromchat.server_config_title
import ru.fromchat.server_url_hint
import ru.fromchat.server_url_label
import ru.fromchat.ui.LocalNavController
@@ -63,16 +62,16 @@ fun ServerConfigScreen() {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
// Load existing config if available
var serverUrl by remember { mutableStateOf("") }
var httpsEnabled by remember { mutableStateOf(false) }
var serverUrl by remember { mutableStateOf("fromchat.ru") }
var httpsEnabled by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
val config = Config.serverConfig.value
if (config != null) {
serverUrl = config.serverUrl
httpsEnabled = config.httpsEnabled
Config.serverConfig.value?.let {
serverUrl = it.serverUrl
httpsEnabled = it.httpsEnabled
}
}
var isLoading by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
@@ -82,11 +81,13 @@ fun ServerConfigScreen() {
TopAppBar(
title = {},
navigationIcon = {
IconButton(onClick = navController::navigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(Res.string.back)
)
if (navController.currentBackStackEntry != null) {
IconButton(onClick = navController::navigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(Res.string.back)
)
}
}
},
scrollBehavior = scrollBehavior
@@ -122,7 +123,7 @@ fun ServerConfigScreen() {
value = serverUrl,
onValueChange = { serverUrl = it },
label = { Text(stringResource(Res.string.server_url_label)) },
placeholder = { Text(stringResource(Res.string.server_url_hint)) },
placeholder = { Text("fromchat.ru") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
@@ -147,16 +148,13 @@ fun ServerConfigScreen() {
onClick = {
isLoading = true
scope.launch {
val config = ServerConfigData(serverUrl, httpsEnabled)
Config.updateServerConfig(config)
Config.updateServerConfig(ServerConfigData(serverUrl, httpsEnabled))
// Ensure we're logged out when server config changes
try {
runCatching {
ApiClient.token?.let { token ->
ApiClient.logout(token)
}
} catch (e: Exception) {
// Ignore logout errors
}
// Clear API client state
@@ -17,34 +17,31 @@ object ServerConfigStorage {
private const val SERVER_URL_KEY = "server_url"
private const val HTTPS_ENABLED_KEY = "https_enabled"
suspend fun getServerUrl(): String? {
val url = storage.getString(SERVER_URL_KEY)
return url.ifEmpty { null }
}
suspend fun getServerUrl() = storage.getString(SERVER_URL_KEY).ifEmpty { null }
suspend fun setServerUrl(url: String) {
storage.putString(SERVER_URL_KEY, url)
}
suspend fun setServerUrl(url: String) = storage.putString(SERVER_URL_KEY, url)
suspend fun getHttpsEnabled(): Boolean? {
return if (storage.contains(HTTPS_ENABLED_KEY)) {
suspend fun getHttpsEnabled() =
if (storage.contains(HTTPS_ENABLED_KEY))
storage.getBoolean(HTTPS_ENABLED_KEY, true)
} else {
null
}
}
else null
suspend fun setHttpsEnabled(enabled: Boolean) {
storage.putBoolean(HTTPS_ENABLED_KEY, enabled)
}
suspend fun hasConfiguration(): Boolean {
return getServerUrl() != null
}
suspend fun hasConfiguration() = getServerUrl() != null && getHttpsEnabled() != null
suspend fun getConfig(): ServerConfigData {
val url = getServerUrl() ?: throw IllegalStateException("Server URL not found in storage")
val https = getHttpsEnabled() ?: true
var url = getServerUrl()
var https = getHttpsEnabled()
if (url == null || https == null) {
url = "fromchat.ru"
https = true
saveConfig(ServerConfigData(url, https))
}
return ServerConfigData(url, https)
}