From c5f4b4f62f7a0f511d3d8d5aad76cccfeb01cbf5 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Sun, 7 Dec 2025 15:29:27 +0300 Subject: [PATCH] Implement server config --- .../src/androidMain/AndroidManifest.xml | 6 +- .../src/androidMain/kotlin/ru/fromchat/App.kt | 11 + .../kotlin/ru/fromchat/ui/Theme.android.kt | 19 ++ .../res/xml/network_security_config.xml | 14 + .../composeResources/drawable/app_icon.xml | 17 ++ .../composeResources/values/strings.xml | 21 ++ .../kotlin/ru/fromchat/Constants.kt | 3 - .../kotlin/ru/fromchat/api/ApiClient.kt | 27 +- .../ru/fromchat/api/WebSocketManager.kt | 6 +- .../kotlin/ru/fromchat/core/Settings.kt | 17 ++ .../kotlin/ru/fromchat/core/config/Config.kt | 55 ++++ .../kotlin/ru/fromchat/ui/AboutScreen.kt | 105 ++++++++ .../commonMain/kotlin/ru/fromchat/ui/App.kt | 151 +++++++---- .../ru/fromchat/ui/NavControllerExtensions.kt | 19 ++ .../commonMain/kotlin/ru/fromchat/ui/Theme.kt | 178 +++++++------ .../kotlin/ru/fromchat/ui/main/MainScreen.kt | 18 +- .../kotlin/ru/fromchat/ui/main/SettingsTab.kt | 248 ++++++++++++++++++ .../kotlin/ru/fromchat/ui/main/TabBase.kt | 37 +++ .../fromchat/ui/setup/ServerConfigScreen.kt | 183 +++++++++++++ .../kotlin/ru/fromchat/ui/Theme.ios.kt | 10 + iosApp/iosApp/Info.plist | 5 + .../com/pr0gramm3r101/utils/UtilsLibrary.kt | 7 +- 22 files changed, 1016 insertions(+), 141 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/ru/fromchat/App.kt create mode 100644 composeApp/src/androidMain/kotlin/ru/fromchat/ui/Theme.android.kt create mode 100644 composeApp/src/androidMain/res/xml/network_security_config.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/app_icon.xml create mode 100644 composeApp/src/commonMain/kotlin/ru/fromchat/core/Settings.kt create mode 100644 composeApp/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt create mode 100644 composeApp/src/commonMain/kotlin/ru/fromchat/ui/AboutScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/ru/fromchat/ui/NavControllerExtensions.kt create mode 100644 composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt create mode 100644 composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/TabBase.kt create mode 100644 composeApp/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt create mode 100644 composeApp/src/iosMain/kotlin/ru/fromchat/ui/Theme.ios.kt diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index a55b7f4..8d220a8 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,6 +1,8 @@ + + + android:usesCleartextTraffic="true" + android:name=".App" + android:networkSecurityConfig="@xml/network_security_config"> = 31) { + if (darkTheme) dynamicDarkColorScheme(LocalContext.current) + else dynamicLightColorScheme(LocalContext.current) + } else { + if (darkTheme) darkColorScheme() + else lightColorScheme() + } \ No newline at end of file diff --git a/composeApp/src/androidMain/res/xml/network_security_config.xml b/composeApp/src/androidMain/res/xml/network_security_config.xml new file mode 100644 index 0000000..2681a9e --- /dev/null +++ b/composeApp/src/androidMain/res/xml/network_security_config.xml @@ -0,0 +1,14 @@ + + + + + + + + + + localhost + 127.0.0.1 + 10.0.2.2 + + diff --git a/composeApp/src/commonMain/composeResources/drawable/app_icon.xml b/composeApp/src/commonMain/composeResources/drawable/app_icon.xml new file mode 100644 index 0000000..4db197b --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/app_icon.xml @@ -0,0 +1,17 @@ + + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index b98b4cb..7968c74 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -7,6 +7,7 @@ Settings Home About app + A 100% free, open source and private messenger. Welcome! @@ -36,6 +37,26 @@ You: Last message Type a message... + + Server Configuration + Enter server details to connect + Server URL + fromchat.ru + Use HTTPS + Save & Continue + + + + Change Server + Changes the FromChat instance you are connecting to. This will log you out of your account. + Logout + Enable Material You theming + Enable the dynamic theme based on your wallpaper. Works only on Android 12 or higher. + Theme + As system + Light + Dark + Unexpected error Invalid username or password diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/Constants.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/Constants.kt index b5b04a0..909c8d1 100644 --- a/composeApp/src/commonMain/kotlin/ru/fromchat/Constants.kt +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/Constants.kt @@ -3,9 +3,6 @@ package ru.fromchat import kotlinx.datetime.LocalDateTime import kotlinx.datetime.format.char -const val API_HOST = "https://fromchat.ru" -const val WS_API_HOST = "wss://fromchat.ru" - val DATETIME_FORMAT = LocalDateTime.Format { day() char('.') diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt index 66dc638..3e47b8b 100644 --- a/composeApp/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt @@ -18,7 +18,7 @@ import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import ru.fromchat.API_HOST +import ru.fromchat.core.config.Config import ru.fromchat.utils.failOnError import kotlin.concurrent.Volatile @@ -52,15 +52,13 @@ object ApiClient { @Volatile var token: String? = null - private set @Volatile var user: User? = null - private set suspend fun login(request: LoginRequest) = http - .post("$API_HOST/api/login") { + .post("${Config.getApiBaseUrl()}/login") { contentType(ContentType.Application.Json) setBody(request.also { ru.fromchat.core.Logger.d("ApiClient", "Login request: $it") }) } @@ -73,7 +71,7 @@ object ApiClient { suspend fun register(request: RegisterRequest) = http - .post("$API_HOST/api/register") { + .post("${Config.getApiBaseUrl()}/register") { contentType(ContentType.Application.Json) setBody(request) } @@ -81,7 +79,7 @@ object ApiClient { suspend fun getMessages() = http - .get("$API_HOST/api/get_messages") { + .get("${Config.getApiBaseUrl()}/get_messages") { contentType(ContentType.Application.Json) } .failOnError() @@ -89,7 +87,7 @@ object ApiClient { suspend fun send(message: String) = http - .post("$API_HOST/api/send_message") { + .post("${Config.getApiBaseUrl()}/send_message") { contentType(ContentType.Application.Json) bearerAuth(token!!) setBody(SendMessageRequest(message)) @@ -99,7 +97,7 @@ object ApiClient { suspend fun editMessage(messageId: Int, content: String) = http - .put("$API_HOST/api/edit_message/$messageId") { + .put("${Config.getApiBaseUrl()}/edit_message/$messageId") { contentType(ContentType.Application.Json) bearerAuth(token!!) setBody(EditMessageRequest(content)) @@ -109,9 +107,20 @@ object ApiClient { suspend fun deleteMessage(messageId: Int) = http - .delete("$API_HOST/api/delete_message/$messageId") { + .delete("${Config.getApiBaseUrl()}/delete_message/$messageId") { contentType(ContentType.Application.Json) bearerAuth(token!!) } .failOnError() + + suspend fun logout(authToken: String) { + // Don't throw on logout errors, just try to logout + try { + http.get("${Config.getApiBaseUrl()}/logout") { + bearerAuth(authToken) + } + } catch (e: Exception) { + // Ignore logout errors + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/api/WebSocketManager.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/api/WebSocketManager.kt index e5e2abd..3e37770 100644 --- a/composeApp/src/commonMain/kotlin/ru/fromchat/api/WebSocketManager.kt +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/api/WebSocketManager.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.Json -import ru.fromchat.WS_API_HOST +import ru.fromchat.core.config.Config import ru.fromchat.core.Logger import kotlin.concurrent.Volatile import kotlin.coroutines.suspendCoroutine @@ -55,10 +55,12 @@ object WebSocketManager { scope.launch { while (isActive) { try { + val wsUrl = Config.getWebSocketUrl() + Logger.d("WebSocketManager", "Connecting to: $wsUrl") ApiClient.http.webSocket( method = HttpMethod.Get, request = { - url("$WS_API_HOST/api/chat/ws") + url(wsUrl) } ) { session = this diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/core/Settings.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/core/Settings.kt new file mode 100644 index 0000000..4cf302d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/core/Settings.kt @@ -0,0 +1,17 @@ +package ru.fromchat.core + +import ru.fromchat.ui.Theme +import com.pr0gramm3r101.utils.settings.Settings + +object Settings { + private val settings = Settings.Companion() + + var materialYou: Boolean + get() = settings.getBoolean("materialYou", true) + set(value) = settings.putBoolean("materialYou", value) + + var theme: Theme + get() = Theme.entries[settings.getInt("theme", Theme.AsSystem.ordinal)] + set(value) = settings.putInt("theme", value.ordinal) +} + diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt new file mode 100644 index 0000000..b1ba025 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt @@ -0,0 +1,55 @@ +package ru.fromchat.core.config + +import com.pr0gramm3r101.utils.storage.ServerConfigData +import com.pr0gramm3r101.utils.storage.ServerConfigStorage +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Application configuration + */ +object Config { + private val _serverConfig = MutableStateFlow(null) + val serverConfig: StateFlow = _serverConfig.asStateFlow() + + /** + * Initialize configuration by loading from storage + */ + suspend fun initialize() { + _serverConfig.value = ServerConfigStorage.getConfig() + } + + /** + * Update server configuration + */ + suspend fun updateServerConfig(config: ServerConfigData) { + ServerConfigStorage.saveConfig(config) + _serverConfig.value = config + } + + /** + * Get API base URL based on current server configuration + */ + fun getApiBaseUrl(): String { + val config = _serverConfig.value ?: ServerConfigData("fromchat.ru", true) + val protocol = if (config.httpsEnabled) "https" else "http" + return "$protocol://${config.serverUrl}/api" + } + + /** + * Get WebSocket URL based on current server configuration + */ + fun getWebSocketUrl(): String { + val config = _serverConfig.value ?: ServerConfigData("fromchat.ru", true) + val protocol = if (config.httpsEnabled) "wss" else "ws" + return "$protocol://${config.serverUrl}/api/chat/ws" + } + + /** + * Checks if server configuration exists + */ + suspend fun hasServerConfig(): Boolean { + return ServerConfigStorage.hasConfiguration() + } +} diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/AboutScreen.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/AboutScreen.kt new file mode 100644 index 0000000..cbf77ca --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/AboutScreen.kt @@ -0,0 +1,105 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package ru.fromchat.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import ru.fromchat.Res +import ru.fromchat.about +import ru.fromchat.app_desc +import ru.fromchat.app_icon +import ru.fromchat.app_name +import ru.fromchat.back + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +inline fun AboutScreen() { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + val navController = LocalNavController.current + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + MediumTopAppBar( + title = { + Text( + stringResource(Res.string.about), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.back) + ) + } + }, + scrollBehavior = scrollBehavior + ) + }, + ) { innerPadding -> + Box(Modifier.padding(innerPadding)) { + Column( + Modifier + .padding(top = 16.dp, start = 16.dp, end = 16.dp) + .verticalScroll(rememberScrollState()) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box(Modifier.padding(bottom = 10.dp)) { + Image( + painter = painterResource(Res.drawable.app_icon), + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .size(70.dp) + ) + } + Text( + text = stringResource(Res.string.app_name), + fontSize = 20.sp, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + Text( + text = stringResource(Res.string.app_desc), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/App.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/App.kt index bc3a2a9..65ac659 100644 --- a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/App.kt +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/App.kt @@ -3,82 +3,145 @@ 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.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.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 import ru.fromchat.ui.auth.RegisterScreen import ru.fromchat.ui.main.MainScreen +import ru.fromchat.ui.setup.ServerConfigScreen val LocalNavController = compositionLocalOf { error("") } @Composable fun App() { + var startDestination by remember { mutableStateOf(null) } + + // Initialize config and check server configuration on startup LaunchedEffect(Unit) { - WebSocketManager.connect() + 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" + } + } + } } - FromChatTheme(dynamicColor = false) { + FromChatTheme { val navController = rememberNavController() val animationSpec = tween(400) CompositionLocalProvider( LocalNavController provides navController ) { - 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 - ) + if (startDestination == null) { + Box( + modifier = androidx.compose.ui.Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } - ) { - composable("login") { - LoginScreen( - onLoginSuccess = { - navController.navigate("chat") - navController.popBackStack() + } else { + NavHost( + navController = navController, + startDestination = startDestination!!, + enterTransition = { + slideIntoContainer( + Start, + animationSpec = animationSpec + ) }, - onNavigateToRegister = { navController.navigate("register") } - ) - } + exitTransition = { + slideOutOfContainer( + Start, + animationSpec = animationSpec + ) + }, + popEnterTransition = { + slideIntoContainer( + End, + animationSpec = animationSpec + ) + }, + popExitTransition = { + slideOutOfContainer( + End, + animationSpec = animationSpec + ) + } + ) { + composable("serverConfig") { + ServerConfigScreen() + } + + composable("login") { + LoginScreen( + onLoginSuccess = { + // Ensure WebSocket is connected after login + WebSocketManager.connect() + navController.navigate("chat") { + popUpTo("login") { inclusive = true } + } + }, + onNavigateToRegister = { navController.navigate("register") } + ) + } - composable("register") { - RegisterScreen( - onRegistered = { navController.navigate("login") } - ) - } + composable("register") { + RegisterScreen( + onRegistered = { navController.navigate("login") } + ) + } - composable("chat") { MainScreen() } + composable("chat") { + MainScreen( + onLogout = { + navController.navigate("login") { + popUpTo("chat") { inclusive = true } + } + } + ) + } - composable("chats/publicChat") { /* PublicChatScreen() */ } + composable("chats/publicChat") { /* PublicChatScreen() */ } + + composable("about") { + AboutScreen() + } + } } } } diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/NavControllerExtensions.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/NavControllerExtensions.kt new file mode 100644 index 0000000..c9dcce7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/NavControllerExtensions.kt @@ -0,0 +1,19 @@ +package ru.fromchat.ui + +import androidx.navigation.NavController + +/** + * Clears the entire back stack except the current screen + */ +fun NavController.wipeBackStack() { + // Keep popping until there are no previous entries + var entry = previousBackStackEntry + while (entry != null) { + val destinationId = entry.destination.id + val success = popBackStack(destinationId, inclusive = true) + if (!success) break + // Check again after popping + entry = previousBackStackEntry + } +} + diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/Theme.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/Theme.kt index 898789b..600a91b 100644 --- a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/Theme.kt +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/Theme.kt @@ -1,95 +1,117 @@ package ru.fromchat.ui +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import ru.fromchat.core.Settings -private val DarkColorScheme = darkColorScheme( - primary = Color(0xFFDBB9F9), - onPrimary = Color(0xFF3E2458), - primaryContainer = Color(0xFF563B71), - onPrimaryContainer = Color(0xFFF0DBFF), - secondary = Color(0xFFD0C1DA), - onSecondary = Color(0xFF362C3F), - secondaryContainer = Color(0xFF4D4356), - onSecondaryContainer = Color(0xFFEDDDF6), - tertiary = Color(0xFFF3B7BE), - onTertiary = Color(0xFF4B252B), - tertiaryContainer = Color(0xFF653A40), - onTertiaryContainer = Color(0xFFFFD9DD), - error = Color(0xFFFFB4AB), - onError = Color(0xFF690005), - errorContainer = Color(0xFF93000A), - onErrorContainer = Color(0xFFFFDAD6), - background = Color(0xFF151218), - onBackground = Color(0xFFE8E0E8), - surface = Color(0xFF151218), - onSurface = Color(0xFFE8E0E8), - surfaceVariant = Color(0xFF4A454E), - onSurfaceVariant = Color(0xFFCCC4CE), - outline = Color(0xFF968E98), - outlineVariant = Color(0xFF4A454E), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFFE8E0E8), - inverseOnSurface = Color(0xFF332F35), - inversePrimary = Color(0xFF6F528A), - surfaceDim = Color(0xFF151218), - surfaceBright = Color(0xFF3C383E), - surfaceContainerLowest = Color(0xFF100D12), - surfaceContainerLow = Color(0xFF1E1A20), - surfaceContainer = Color(0xFF221E24), - surfaceContainerHigh = Color(0xFF2C292E), - surfaceContainerHighest = Color(0xFF373339), +enum class Theme { + AsSystem, + Light, + Dark +} + +var dynamicThemeEnabled by mutableStateOf( + runCatching { Settings.materialYou }.getOrNull() == true ) -private val LightColorScheme = lightColorScheme( - primary = Color(0xFF1F6586), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xFFC5E7FF), - onPrimaryContainer = Color(0xFF004C6A), - secondary = Color(0xFF4E616D), - onSecondary = Color(0xFFFFFFFF), - secondaryContainer = Color(0xFFD2E5F4), - onSecondaryContainer = Color(0xFF374955), - tertiary = Color(0xFF615A7C), - onTertiary = Color(0xFFFFFFFF), - tertiaryContainer = Color(0xFFE7DEFF), - onTertiaryContainer = Color(0xFF494263), - error = Color(0xFFBA1A1A), - onError = Color(0xFFFFFFFF), - errorContainer = Color(0xFFFFDAD6), - onErrorContainer = Color(0xFF93000A), - background = Color(0xFFF6FAFE), - onBackground = Color(0xFF181C1F), - surface = Color(0xFFF6FAFE), - onSurface = Color(0xFF181C1F), - surfaceVariant = Color(0xFFDDE3EA), - onSurfaceVariant = Color(0xFF41484D), - outline = Color(0xFF71787E), - outlineVariant = Color(0xFFC1C7CE), - scrim = Color(0xFF000000), - inverseSurface = Color(0xFF2C3134), - inverseOnSurface = Color(0xFFEDF1F5), - inversePrimary = Color(0xFF91CEF4), - surfaceDim = Color(0xFFD7DADF), - surfaceBright = Color(0xFFF6FAFE), - surfaceContainerLowest = Color(0xFFFFFFFF), - surfaceContainerLow = Color(0xFFF0F4F8), - surfaceContainer = Color(0xFFEBEEF3), - surfaceContainerHigh = Color(0xFFE5E8ED), - surfaceContainerHighest = Color(0xFFDFE3E7), +var theme by mutableStateOf( + runCatching { Settings.theme }.getOrNull() ?: Theme.AsSystem ) +@Composable +expect fun getColorScheme(darkTheme: Boolean, dynamicColor: Boolean): ColorScheme + @Composable fun FromChatTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - dynamicColor: Boolean = false, // Disabled for multiplatform compatibility + darkTheme: Boolean = when (theme) { + Theme.AsSystem -> isSystemInDarkTheme() + Theme.Light -> false + Theme.Dark -> true + }, + dynamicColor: Boolean = dynamicThemeEnabled, content: @Composable () -> Unit ) { - val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme + var colorScheme = getColorScheme(darkTheme, dynamicColor) + + val primary by animateColorAsState(colorScheme.primary) + val onPrimary by animateColorAsState(colorScheme.onPrimary) + val primaryContainer by animateColorAsState(colorScheme.primaryContainer) + val onPrimaryContainer by animateColorAsState(colorScheme.onPrimaryContainer) + val secondary by animateColorAsState(colorScheme.secondary) + val onSecondary by animateColorAsState(colorScheme.onSecondary) + val secondaryContainer by animateColorAsState(colorScheme.secondaryContainer) + val onSecondaryContainer by animateColorAsState(colorScheme.onSecondaryContainer) + val tertiary by animateColorAsState(colorScheme.tertiary) + val onTertiary by animateColorAsState(colorScheme.onTertiary) + val tertiaryContainer by animateColorAsState(colorScheme.tertiaryContainer) + val onTertiaryContainer by animateColorAsState(colorScheme.onTertiaryContainer) + val error by animateColorAsState(colorScheme.error) + val onError by animateColorAsState(colorScheme.onError) + val errorContainer by animateColorAsState(colorScheme.errorContainer) + val onErrorContainer by animateColorAsState(colorScheme.onErrorContainer) + val background by animateColorAsState(colorScheme.background) + val onBackground by animateColorAsState(colorScheme.onBackground) + val surface by animateColorAsState(colorScheme.surface) + val onSurface by animateColorAsState(colorScheme.onSurface) + val surfaceVariant by animateColorAsState(colorScheme.surfaceVariant) + val onSurfaceVariant by animateColorAsState(colorScheme.onSurfaceVariant) + val outline by animateColorAsState(colorScheme.outline) + val outlineVariant by animateColorAsState(colorScheme.outlineVariant) + val scrim by animateColorAsState(colorScheme.scrim) + val inverseSurface by animateColorAsState(colorScheme.inverseSurface) + val inverseOnSurface by animateColorAsState(colorScheme.inverseOnSurface) + val inversePrimary by animateColorAsState(colorScheme.inversePrimary) + val surfaceDim by animateColorAsState(colorScheme.surfaceDim) + val surfaceBright by animateColorAsState(colorScheme.surfaceBright) + val surfaceContainerLowest by animateColorAsState(colorScheme.surfaceContainerLowest) + val surfaceContainerLow by animateColorAsState(colorScheme.surfaceContainerLow) + val surfaceContainer by animateColorAsState(colorScheme.surfaceContainer) + val surfaceContainerHigh by animateColorAsState(colorScheme.surfaceContainerHigh) + val surfaceContainerHighest by animateColorAsState(colorScheme.surfaceContainerHighest) + + colorScheme = colorScheme.copy( + primary = primary, + onPrimary = onPrimary, + primaryContainer = primaryContainer, + onPrimaryContainer = onPrimaryContainer, + secondary = secondary, + onSecondary = onSecondary, + secondaryContainer = secondaryContainer, + onSecondaryContainer = onSecondaryContainer, + tertiary = tertiary, + onTertiary = onTertiary, + tertiaryContainer = tertiaryContainer, + onTertiaryContainer = onTertiaryContainer, + error = error, + onError = onError, + errorContainer = errorContainer, + onErrorContainer = onErrorContainer, + background = background, + onBackground = onBackground, + surface = surface, + onSurface = onSurface, + surfaceVariant = surfaceVariant, + onSurfaceVariant = onSurfaceVariant, + outline = outline, + outlineVariant = outlineVariant, + scrim = scrim, + inverseSurface = inverseSurface, + inverseOnSurface = inverseOnSurface, + inversePrimary = inversePrimary, + surfaceDim = surfaceDim, + surfaceBright = surfaceBright, + surfaceContainerLowest = surfaceContainerLowest, + surfaceContainerLow = surfaceContainerLow, + surfaceContainer = surfaceContainer, + surfaceContainerHigh = surfaceContainerHigh, + surfaceContainerHighest = surfaceContainerHighest + ) MaterialTheme( colorScheme = colorScheme, diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/MainScreen.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/MainScreen.kt index a519426..b0afdfc 100644 --- a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/MainScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Chat import androidx.compose.material.icons.filled.Contacts import androidx.compose.material.icons.filled.Mail +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem @@ -20,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import org.jetbrains.compose.resources.stringResource @@ -28,12 +30,15 @@ import ru.fromchat.chats import ru.fromchat.coming_soon import ru.fromchat.contacts import ru.fromchat.dms +import ru.fromchat.settings +import ru.fromchat.ui.LocalNavController import ru.fromchat.utils.exclude @Suppress("AssignedValueIsNeverRead") @Composable -fun MainScreen() { - var selectedTab by remember { mutableStateOf("chats") } +fun MainScreen(onLogout: () -> Unit = {}) { + var selectedTab by rememberSaveable { mutableStateOf("chats") } + val navController = LocalNavController.current Scaffold( bottomBar = { @@ -56,6 +61,12 @@ fun MainScreen() { label = { Text(stringResource(Res.string.dms)) }, icon = { Icon(Icons.Filled.Mail, contentDescription = null) } ) + NavigationBarItem( + selected = selectedTab == "settings", + onClick = { selectedTab = "settings" }, + label = { Text(stringResource(Res.string.settings)) }, + icon = { Icon(Icons.Filled.Settings, contentDescription = null) } + ) } }, contentWindowInsets = WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top), @@ -74,6 +85,9 @@ fun MainScreen() { "dms" -> { Text(stringResource(Res.string.coming_soon)) } + "settings" -> { + SettingsTab(onLogout = onLogout) + } } } } diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt new file mode 100644 index 0000000..2988cf5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/SettingsTab.kt @@ -0,0 +1,248 @@ +package ru.fromchat.ui.main + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material.icons.filled.Brush +import androidx.compose.material.icons.filled.DarkMode +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.LightMode +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Storage +import androidx.compose.material.icons.filled.Wallpaper +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.pr0gramm3r101.components.Category +import com.pr0gramm3r101.components.ListItem +import com.pr0gramm3r101.components.SwitchListItem +import com.pr0gramm3r101.utils.materialYouAvailable +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import ru.fromchat.Res +import ru.fromchat.about +import ru.fromchat.api.ApiClient +import ru.fromchat.api.WebSocketManager +import ru.fromchat.as_system +import ru.fromchat.change_server +import ru.fromchat.change_server_d +import ru.fromchat.core.Settings +import ru.fromchat.dark +import ru.fromchat.light +import ru.fromchat.logout +import ru.fromchat.materialYou +import ru.fromchat.materialYou_d +import ru.fromchat.settings +import ru.fromchat.theme +import ru.fromchat.ui.LocalNavController +import ru.fromchat.ui.Theme +import ru.fromchat.ui.dynamicThemeEnabled +import ru.fromchat.ui.theme + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun SettingsTab( + onLogout: () -> Unit +) { + TabBase { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + val navController = LocalNavController.current + val scope = rememberCoroutineScope() + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + MediumTopAppBar( + title = { + Text( + text = stringResource(Res.string.settings), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + actions = { + IconButton(onClick = { navController.navigate("about") }) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = stringResource(Res.string.about) + ) + } + }, + scrollBehavior = scrollBehavior + ) + }, + ) { innerPadding -> + Column( + Modifier + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + ) { + var materialYouSwitch by remember { + mutableStateOf( + Settings.materialYou && materialYouAvailable + ) + } + + Category(Modifier.padding(top = 16.dp)) { + // Material You + SwitchListItem( + headline = stringResource(Res.string.materialYou), + supportingText = stringResource(Res.string.materialYou_d), + enabled = materialYouAvailable, + checked = materialYouSwitch, + onCheckedChange = { + materialYouSwitch = it + Settings.materialYou = it + dynamicThemeEnabled = it + }, + divider = true, + dividerColor = MaterialTheme.colorScheme.surface, + dividerThickness = 2.dp, + leadingContent = { + Icon( + imageVector = Icons.Filled.Wallpaper, + contentDescription = null + ) + } + ) + + // Theme + ListItem( + headline = stringResource(Res.string.theme), + leadingContent = { + Icon( + imageVector = Icons.Filled.Brush, + contentDescription = null + ) + }, + bottomContent = { + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + var selectedIndex by remember { mutableIntStateOf(Settings.theme.ordinal) } + val options = listOf( + stringResource(Res.string.as_system), + stringResource(Res.string.light), + stringResource(Res.string.dark) + ) + options.forEachIndexed { index, label -> + FilterChip( + onClick = { + selectedIndex = index + Settings.theme = Theme.entries[index] + theme = Theme.entries[index] + }, + selected = index == selectedIndex, + leadingIcon = { + if (index == 0) { + Spacer(Modifier.width(16.dp)) + } + when (index) { + 0 -> Icon(Icons.Filled.Settings, null) + 1 -> Icon(Icons.Filled.LightMode, null) + 2 -> Icon(Icons.Filled.DarkMode, null) + } + }, + label = { + Text( + text = label, + overflow = TextOverflow.Ellipsis + ) + } + ) + } + } + } + ) + } + + // Server Configuration + Category(Modifier.padding(top = 16.dp)) { + ListItem( + headline = stringResource(Res.string.change_server), + supportingText = stringResource(Res.string.change_server_d), + 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") + } + }, + divider = true, + dividerColor = MaterialTheme.colorScheme.surface, + dividerThickness = 2.dp, + leadingContent = { + Icon(Icons.Filled.Storage, null) + } + ) + + ListItem( + headline = stringResource(Res.string.logout), + leadingContent = { + Icon(Icons.AutoMirrored.Filled.Logout, null) + }, + onClick = { + scope.launch { + // Logout + try { + ApiClient.token?.let { token -> + ApiClient.logout(token) + } + } catch (e: Exception) { + // Ignore logout errors + } + + // Clear API client state + ApiClient.token = null + ApiClient.user = null + + // Shutdown WebSocket + WebSocketManager.shutdown() + + // Navigate back to auth + onLogout() + } + } + ) + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/TabBase.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/TabBase.kt new file mode 100644 index 0000000..f47be7e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/main/TabBase.kt @@ -0,0 +1,37 @@ +package ru.fromchat.ui.main + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import com.pr0gramm3r101.utils.resetFocus + +@Composable +inline fun TabBase( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + Box( + modifier = Modifier + .fillMaxSize() + .clickable( + interactionSource = interactionSource, + indication = null + ) { + resetFocus(keyboardController, focusManager) + } + .then(modifier) + ) { + content() + } +} + diff --git a/composeApp/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt new file mode 100644 index 0000000..5ea1dbc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt @@ -0,0 +1,183 @@ +package ru.fromchat.ui.setup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +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.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.dp +import com.pr0gramm3r101.utils.storage.ServerConfigData +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import ru.fromchat.Res +import ru.fromchat.api.ApiClient +import ru.fromchat.api.WebSocketManager +import ru.fromchat.back +import ru.fromchat.core.config.Config +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 +import ru.fromchat.ui.wipeBackStack + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ServerConfigScreen() { + val navController = LocalNavController.current + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + // Load existing config if available + 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 + } + } + var isLoading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = navController::navigateUp) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.back) + ) + } + }, + scrollBehavior = scrollBehavior + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .windowInsetsPadding(WindowInsets.ime) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(Res.string.server_config_title), + style = MaterialTheme.typography.headlineMedium + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(Res.string.server_config_subtitle), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(32.dp)) + + OutlinedTextField( + value = serverUrl, + onValueChange = { serverUrl = it }, + label = { Text(stringResource(Res.string.server_url_label)) }, + placeholder = { Text(stringResource(Res.string.server_url_hint)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = httpsEnabled, + onCheckedChange = { httpsEnabled = it } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(Res.string.https_enabled)) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = { + isLoading = true + scope.launch { + val config = ServerConfigData(serverUrl, httpsEnabled) + Config.updateServerConfig(config) + + // Ensure we're logged out when server config changes + try { + ApiClient.token?.let { token -> + ApiClient.logout(token) + } + } catch (e: Exception) { + // Ignore logout errors + } + + // Clear API client state + ApiClient.token = null + ApiClient.user = null + + // Shutdown WebSocket (will reconnect on login) + WebSocketManager.shutdown() + + // Clear entire back stack except current screen + navController.wipeBackStack() + + // Navigate to login + navController.navigate("login") + } + }, + enabled = !isLoading && serverUrl.isNotBlank(), + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.save_continue)) + } + } + } +} diff --git a/composeApp/src/iosMain/kotlin/ru/fromchat/ui/Theme.ios.kt b/composeApp/src/iosMain/kotlin/ru/fromchat/ui/Theme.ios.kt new file mode 100644 index 0000000..a9e0885 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/ru/fromchat/ui/Theme.ios.kt @@ -0,0 +1,10 @@ +package ru.fromchat.ui + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +@Composable +actual fun getColorScheme(darkTheme: Boolean, dynamicColor: Boolean) = + if (darkTheme) darkColorScheme() + else lightColorScheme() \ No newline at end of file diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index aaf5e18..c013cf3 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -46,5 +46,10 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/utils/src/androidMain/kotlin/com/pr0gramm3r101/utils/UtilsLibrary.kt b/utils/src/androidMain/kotlin/com/pr0gramm3r101/utils/UtilsLibrary.kt index c896098..66990ce 100644 --- a/utils/src/androidMain/kotlin/com/pr0gramm3r101/utils/UtilsLibrary.kt +++ b/utils/src/androidMain/kotlin/com/pr0gramm3r101/utils/UtilsLibrary.kt @@ -6,9 +6,12 @@ import java.lang.ref.WeakReference actual object UtilsLibrary { private var init = false private lateinit var _context: WeakReference - internal val context get() = _context.get()!! + val context get() = _context.get()!! actual fun init(vararg args: Any) { - _context = WeakReference(args[0] as Context) + if (!init) { + _context = WeakReference(args[0] as Context) + init = true + } } } \ No newline at end of file