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)
}