From d28fadc35204d8aa98e7a05ddb883b860a1507ac Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Mon, 22 Dec 2025 16:09:27 +0300 Subject: [PATCH] Redesign server config --- .../composeResources/values/strings.xml | 2 + .../kotlin/ru/fromchat/core/config/Config.kt | 3 + .../commonMain/kotlin/ru/fromchat/ui/App.kt | 176 +++++++----------- .../kotlin/ru/fromchat/ui/auth/LoginScreen.kt | 61 +++++- .../kotlin/ru/fromchat/ui/main/SettingsTab.kt | 14 -- .../fromchat/ui/setup/ServerConfigScreen.kt | 34 ++-- .../utils/storage/ServerConfigStorage.kt | 33 ++-- 7 files changed, 164 insertions(+), 159 deletions(-) diff --git a/app/shared/src/commonMain/composeResources/values/strings.xml b/app/shared/src/commonMain/composeResources/values/strings.xml index e5aadbb..45c5c64 100644 --- a/app/shared/src/commonMain/composeResources/values/strings.xml +++ b/app/shared/src/commonMain/composeResources/values/strings.xml @@ -67,4 +67,6 @@ %1$s is typing… %1$s and %2$s are typing… %1$s, %2$s and %3$d more are typing… + + More diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt index ae8632a..9fd35c4 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt @@ -21,6 +21,9 @@ object Config { */ suspend fun initialize() { _serverConfig.value = ServerConfigStorage.getConfig() + + // Set default values and prevent corruption + ServerConfigStorage.getConfig() } /** diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/App.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/App.kt index 633b050..ec691bb 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/App.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/App.kt @@ -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 { error("") } @Composable fun App() { - var startDestination by remember { mutableStateOf(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() + } + } } } } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/LoginScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/LoginScreen.kt index 41e1955..a64033c 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/LoginScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/LoginScreen.kt @@ -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(null) } diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt index 2988cf5..1556704 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt @@ -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") } }, diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt index 78bdebe..b1afccf 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt @@ -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 diff --git a/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/utils/storage/ServerConfigStorage.kt b/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/utils/storage/ServerConfigStorage.kt index b5d28d5..26818a9 100644 --- a/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/utils/storage/ServerConfigStorage.kt +++ b/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/utils/storage/ServerConfigStorage.kt @@ -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) }