mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Implement server config
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -10,7 +12,9 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.FromChat"
|
android:theme="@style/Theme.FromChat"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true"
|
||||||
|
android:name=".App"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ru.fromchat
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.pr0gramm3r101.utils.UtilsLibrary
|
||||||
|
|
||||||
|
class App: Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
UtilsLibrary.init(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ru.fromchat.ui
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun getColorScheme(darkTheme: Boolean, dynamicColor: Boolean) =
|
||||||
|
if (dynamicColor && Build.VERSION.SDK_INT >= 31) {
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
|
||||||
|
else dynamicLightColorScheme(LocalContext.current)
|
||||||
|
} else {
|
||||||
|
if (darkTheme) darkColorScheme()
|
||||||
|
else lightColorScheme()
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="true">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
<certificates src="user" />
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="true">localhost</domain>
|
||||||
|
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||||
|
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||||
|
</domain-config>
|
||||||
|
</network-security-config>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:pathData="M0 0 h 1024 v 1024 h -1024 L0 0"
|
||||||
|
android:fillColor="#4484f4"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M500.1,129.1C497.2,129.6 491.7,131.1 488,132.4C483.9,133.8 427.2,165.8 339.9,216C262.1,260.7 196.7,298.5 194.5,300C188.8,303.9 178.1,315.6 174.2,322.2C170,329.3 166.4,339.6 165,348.5C164.3,353.3 164,407.1 164.2,522L164.5,688.5 167.1,696.2C172.4,711.6 183,726 195.3,734.3C205,741 481.1,890.9 487.3,892.9C504.8,898.6 523.3,897.9 539.4,891.1C547.3,887.7 827.3,721.9 833.7,716.8C841.4,710.7 848.2,701.9 853.1,691.5C860.4,675.9 860,686.3 860,509.5C860,331.6 860.5,344.2 852.5,327.5C846.2,314.4 837,304.3 824.3,296.7C811.8,289.2 549.2,138.1 544.5,135.8C532.1,129.5 513.5,126.7 500.1,129.1M502.5,149.4C502,149.6 499,150.6 496,151.5C489,153.7 208.7,314.7 202.8,320C200.4,322.1 198.7,324 199,324.3C199.3,324.6 225.1,339.5 256.5,357.5C292.1,377.9 313.9,389.9 314.6,389.3C315.3,388.9 317.3,387.3 319.1,385.8C324.2,381.9 492.9,284.6 498.6,282.4C505.5,279.7 519.6,279.9 526.5,282.7C532.9,285.2 702.2,383.2 707.2,387.3L710.9,390.3 768.5,357.4L826.1,324.5 821.6,320.4C816.6,315.8 540.6,156.2 530.5,152.1C525.7,150.2 522.3,149.6 514,149.4C508.2,149.2 503,149.2 502.5,149.4M186.2,347.1C184.6,353.8 184.5,680.2 186.1,687C188.1,696 193,704.2 200.4,711.4C207.3,718.1 211.3,720.4 348.5,795.1C426,837.4 491.4,872.6 493.9,873.5C502.9,876.5 502,883.5 502,809.6L502,744.2 497.7,742.7C493.2,741.2 327.2,650.6 320.4,645.9C314.5,641.9 310.2,636.6 306.6,629L303.5,622.5 303.5,515.3L303.5,408.2 245.8,375.1C214,356.9 187.9,342 187.7,342C187.5,342 186.8,344.3 186.2,347.1M779.1,375L721.8,407.9 721.4,507.7C721.1,562.6 720.7,609.3 720.4,611.6C719.6,618 714.5,627.6 709.2,632.8C705.7,636.1 679.1,652.4 617.9,688.7C570.4,716.9 529.3,740.8 526.7,742L522,744.1 522,809.6L522,875 524.8,874.4C530.2,873.2 543.7,865.4 679.1,785.5C828,697.6 826.1,698.9 832.8,686.3C839.7,673.1 839.3,685.9 839.4,509.5C839.5,370.5 839.3,350 838,346.4L836.5,342.2 779.1,375M504.5,357.1C502.4,357.6 474.2,373.2 442,391.8C378.9,428.2 376.8,429.6 373,439.6C371,445.1 371,446.4 371.2,515.8L371.5,586.5 373.8,591.1C375,593.7 377.8,597.5 380,599.7C385.5,605.1 497.7,666.5 505.1,668.1C511.3,669.4 518.9,668.6 524.3,666C533.3,661.6 639.5,598.2 643.3,594.9C648.3,590.6 651.6,584.3 653.1,576.7C653.7,573.1 654,548.2 653.8,507.8L653.5,444.5 650.2,437.8C648.4,434.2 645.5,429.9 643.7,428.3C640.3,425.4 529.5,361 523.4,358.4C518.9,356.5 509.5,355.8 504.5,357.1"
|
||||||
|
android:fillColor="#fafbfc"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="home">Home</string>
|
<string name="home">Home</string>
|
||||||
<string name="about">About app</string>
|
<string name="about">About app</string>
|
||||||
|
<string name="app_desc">A 100% free, open source and private messenger.</string>
|
||||||
|
|
||||||
<!-- Authentication -->
|
<!-- Authentication -->
|
||||||
<string name="welcome">Welcome!</string>
|
<string name="welcome">Welcome!</string>
|
||||||
@@ -36,6 +37,26 @@
|
|||||||
<string name="chat_last_mesaage">You: Last message</string>
|
<string name="chat_last_mesaage">You: Last message</string>
|
||||||
<string name="message_placeholder">Type a message...</string>
|
<string name="message_placeholder">Type a message...</string>
|
||||||
|
|
||||||
|
<!-- Server Configuration -->
|
||||||
|
<string name="server_config_title">Server Configuration</string>
|
||||||
|
<string name="server_config_subtitle">Enter server details to connect</string>
|
||||||
|
<string name="server_url_label">Server URL</string>
|
||||||
|
<string name="server_url_hint">fromchat.ru</string>
|
||||||
|
<string name="https_enabled">Use HTTPS</string>
|
||||||
|
<string name="save_continue">Save & Continue</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<string name="change_server">Change Server</string>
|
||||||
|
<string name="change_server_d">Changes the FromChat instance you are connecting to. This will log you out of your account.</string>
|
||||||
|
<string name="logout">Logout</string>
|
||||||
|
<string name="materialYou">Enable Material You theming</string>
|
||||||
|
<string name="materialYou_d">Enable the dynamic theme based on your wallpaper. Works only on Android 12 or higher.</string>
|
||||||
|
<string name="theme">Theme</string>
|
||||||
|
<string name="as_system">As system</string>
|
||||||
|
<string name="light">Light</string>
|
||||||
|
<string name="dark">Dark</string>
|
||||||
|
|
||||||
<!-- Error Messages -->
|
<!-- Error Messages -->
|
||||||
<string name="error_unexpected">Unexpected error</string>
|
<string name="error_unexpected">Unexpected error</string>
|
||||||
<string name="error_invalid_credentials">Invalid username or password</string>
|
<string name="error_invalid_credentials">Invalid username or password</string>
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ package ru.fromchat
|
|||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.format.char
|
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 {
|
val DATETIME_FORMAT = LocalDateTime.Format {
|
||||||
day()
|
day()
|
||||||
char('.')
|
char('.')
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import io.ktor.http.ContentType
|
|||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.fromchat.API_HOST
|
import ru.fromchat.core.config.Config
|
||||||
import ru.fromchat.utils.failOnError
|
import ru.fromchat.utils.failOnError
|
||||||
import kotlin.concurrent.Volatile
|
import kotlin.concurrent.Volatile
|
||||||
|
|
||||||
@@ -52,15 +52,13 @@ object ApiClient {
|
|||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var token: String? = null
|
var token: String? = null
|
||||||
private set
|
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var user: User? = null
|
var user: User? = null
|
||||||
private set
|
|
||||||
|
|
||||||
suspend fun login(request: LoginRequest) =
|
suspend fun login(request: LoginRequest) =
|
||||||
http
|
http
|
||||||
.post("$API_HOST/api/login") {
|
.post("${Config.getApiBaseUrl()}/login") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(request.also { ru.fromchat.core.Logger.d("ApiClient", "Login request: $it") })
|
setBody(request.also { ru.fromchat.core.Logger.d("ApiClient", "Login request: $it") })
|
||||||
}
|
}
|
||||||
@@ -73,7 +71,7 @@ object ApiClient {
|
|||||||
|
|
||||||
suspend fun register(request: RegisterRequest) =
|
suspend fun register(request: RegisterRequest) =
|
||||||
http
|
http
|
||||||
.post("$API_HOST/api/register") {
|
.post("${Config.getApiBaseUrl()}/register") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(request)
|
setBody(request)
|
||||||
}
|
}
|
||||||
@@ -81,7 +79,7 @@ object ApiClient {
|
|||||||
|
|
||||||
suspend fun getMessages() =
|
suspend fun getMessages() =
|
||||||
http
|
http
|
||||||
.get("$API_HOST/api/get_messages") {
|
.get("${Config.getApiBaseUrl()}/get_messages") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
}
|
}
|
||||||
.failOnError()
|
.failOnError()
|
||||||
@@ -89,7 +87,7 @@ object ApiClient {
|
|||||||
|
|
||||||
suspend fun send(message: String) =
|
suspend fun send(message: String) =
|
||||||
http
|
http
|
||||||
.post("$API_HOST/api/send_message") {
|
.post("${Config.getApiBaseUrl()}/send_message") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
bearerAuth(token!!)
|
bearerAuth(token!!)
|
||||||
setBody(SendMessageRequest(message))
|
setBody(SendMessageRequest(message))
|
||||||
@@ -99,7 +97,7 @@ object ApiClient {
|
|||||||
|
|
||||||
suspend fun editMessage(messageId: Int, content: String) =
|
suspend fun editMessage(messageId: Int, content: String) =
|
||||||
http
|
http
|
||||||
.put("$API_HOST/api/edit_message/$messageId") {
|
.put("${Config.getApiBaseUrl()}/edit_message/$messageId") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
bearerAuth(token!!)
|
bearerAuth(token!!)
|
||||||
setBody(EditMessageRequest(content))
|
setBody(EditMessageRequest(content))
|
||||||
@@ -109,9 +107,20 @@ object ApiClient {
|
|||||||
|
|
||||||
suspend fun deleteMessage(messageId: Int) =
|
suspend fun deleteMessage(messageId: Int) =
|
||||||
http
|
http
|
||||||
.delete("$API_HOST/api/delete_message/$messageId") {
|
.delete("${Config.getApiBaseUrl()}/delete_message/$messageId") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
bearerAuth(token!!)
|
bearerAuth(token!!)
|
||||||
}
|
}
|
||||||
.failOnError()
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.fromchat.WS_API_HOST
|
import ru.fromchat.core.config.Config
|
||||||
import ru.fromchat.core.Logger
|
import ru.fromchat.core.Logger
|
||||||
import kotlin.concurrent.Volatile
|
import kotlin.concurrent.Volatile
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
@@ -55,10 +55,12 @@ object WebSocketManager {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
|
val wsUrl = Config.getWebSocketUrl()
|
||||||
|
Logger.d("WebSocketManager", "Connecting to: $wsUrl")
|
||||||
ApiClient.http.webSocket(
|
ApiClient.http.webSocket(
|
||||||
method = HttpMethod.Get,
|
method = HttpMethod.Get,
|
||||||
request = {
|
request = {
|
||||||
url("$WS_API_HOST/api/chat/ws")
|
url(wsUrl)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
session = this
|
session = this
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<ServerConfigData?>(null)
|
||||||
|
val serverConfig: StateFlow<ServerConfigData?> = _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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,82 +3,145 @@ package ru.fromchat.ui
|
|||||||
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.End
|
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.End
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Start
|
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Start
|
||||||
import androidx.compose.animation.core.tween
|
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.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
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.compose.ui.unit.IntOffset
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import ru.fromchat.api.WebSocketManager
|
import ru.fromchat.api.WebSocketManager
|
||||||
|
import ru.fromchat.core.config.Config
|
||||||
import ru.fromchat.ui.auth.LoginScreen
|
import ru.fromchat.ui.auth.LoginScreen
|
||||||
import ru.fromchat.ui.auth.RegisterScreen
|
import ru.fromchat.ui.auth.RegisterScreen
|
||||||
import ru.fromchat.ui.main.MainScreen
|
import ru.fromchat.ui.main.MainScreen
|
||||||
|
import ru.fromchat.ui.setup.ServerConfigScreen
|
||||||
|
|
||||||
val LocalNavController = compositionLocalOf<NavController> { error("") }
|
val LocalNavController = compositionLocalOf<NavController> { error("") }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App() {
|
||||||
|
var startDestination by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
// Initialize config and check server configuration on startup
|
||||||
LaunchedEffect(Unit) {
|
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 navController = rememberNavController()
|
||||||
val animationSpec = tween<IntOffset>(400)
|
val animationSpec = tween<IntOffset>(400)
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalNavController provides navController
|
LocalNavController provides navController
|
||||||
) {
|
) {
|
||||||
NavHost(
|
if (startDestination == null) {
|
||||||
navController = navController,
|
Box(
|
||||||
startDestination = "login",
|
modifier = androidx.compose.ui.Modifier.fillMaxSize(),
|
||||||
enterTransition = {
|
contentAlignment = Alignment.Center
|
||||||
slideIntoContainer(
|
) {
|
||||||
Start,
|
CircularProgressIndicator()
|
||||||
animationSpec = animationSpec
|
|
||||||
)
|
|
||||||
},
|
|
||||||
exitTransition = {
|
|
||||||
slideOutOfContainer(
|
|
||||||
Start,
|
|
||||||
animationSpec = animationSpec
|
|
||||||
)
|
|
||||||
},
|
|
||||||
popEnterTransition = {
|
|
||||||
slideIntoContainer(
|
|
||||||
End,
|
|
||||||
animationSpec = animationSpec
|
|
||||||
)
|
|
||||||
},
|
|
||||||
popExitTransition = {
|
|
||||||
slideOutOfContainer(
|
|
||||||
End,
|
|
||||||
animationSpec = animationSpec
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
) {
|
} else {
|
||||||
composable("login") {
|
NavHost(
|
||||||
LoginScreen(
|
navController = navController,
|
||||||
onLoginSuccess = {
|
startDestination = startDestination!!,
|
||||||
navController.navigate("chat")
|
enterTransition = {
|
||||||
navController.popBackStack()
|
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") {
|
composable("register") {
|
||||||
RegisterScreen(
|
RegisterScreen(
|
||||||
onRegistered = { navController.navigate("login") }
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,95 +1,117 @@
|
|||||||
package ru.fromchat.ui
|
package ru.fromchat.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.runtime.Composable
|
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(
|
enum class Theme {
|
||||||
primary = Color(0xFFDBB9F9),
|
AsSystem,
|
||||||
onPrimary = Color(0xFF3E2458),
|
Light,
|
||||||
primaryContainer = Color(0xFF563B71),
|
Dark
|
||||||
onPrimaryContainer = Color(0xFFF0DBFF),
|
}
|
||||||
secondary = Color(0xFFD0C1DA),
|
|
||||||
onSecondary = Color(0xFF362C3F),
|
var dynamicThemeEnabled by mutableStateOf(
|
||||||
secondaryContainer = Color(0xFF4D4356),
|
runCatching { Settings.materialYou }.getOrNull() == true
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
var theme by mutableStateOf(
|
||||||
primary = Color(0xFF1F6586),
|
runCatching { Settings.theme }.getOrNull() ?: Theme.AsSystem
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun getColorScheme(darkTheme: Boolean, dynamicColor: Boolean): ColorScheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FromChatTheme(
|
fun FromChatTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = when (theme) {
|
||||||
dynamicColor: Boolean = false, // Disabled for multiplatform compatibility
|
Theme.AsSystem -> isSystemInDarkTheme()
|
||||||
|
Theme.Light -> false
|
||||||
|
Theme.Dark -> true
|
||||||
|
},
|
||||||
|
dynamicColor: Boolean = dynamicThemeEnabled,
|
||||||
content: @Composable () -> Unit
|
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(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||||
import androidx.compose.material.icons.filled.Contacts
|
import androidx.compose.material.icons.filled.Contacts
|
||||||
import androidx.compose.material.icons.filled.Mail
|
import androidx.compose.material.icons.filled.Mail
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
@@ -20,6 +21,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
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.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
@@ -28,12 +30,15 @@ import ru.fromchat.chats
|
|||||||
import ru.fromchat.coming_soon
|
import ru.fromchat.coming_soon
|
||||||
import ru.fromchat.contacts
|
import ru.fromchat.contacts
|
||||||
import ru.fromchat.dms
|
import ru.fromchat.dms
|
||||||
|
import ru.fromchat.settings
|
||||||
|
import ru.fromchat.ui.LocalNavController
|
||||||
import ru.fromchat.utils.exclude
|
import ru.fromchat.utils.exclude
|
||||||
|
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
@Suppress("AssignedValueIsNeverRead")
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen() {
|
fun MainScreen(onLogout: () -> Unit = {}) {
|
||||||
var selectedTab by remember { mutableStateOf("chats") }
|
var selectedTab by rememberSaveable { mutableStateOf("chats") }
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
@@ -56,6 +61,12 @@ fun MainScreen() {
|
|||||||
label = { Text(stringResource(Res.string.dms)) },
|
label = { Text(stringResource(Res.string.dms)) },
|
||||||
icon = { Icon(Icons.Filled.Mail, contentDescription = null) }
|
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),
|
contentWindowInsets = WindowInsets.safeDrawing.exclude(WindowInsetsSides.Top),
|
||||||
@@ -74,6 +85,9 @@ fun MainScreen() {
|
|||||||
"dms" -> {
|
"dms" -> {
|
||||||
Text(stringResource(Res.string.coming_soon))
|
Text(stringResource(Res.string.coming_soon))
|
||||||
}
|
}
|
||||||
|
"settings" -> {
|
||||||
|
SettingsTab(onLogout = onLogout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -46,5 +46,10 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import java.lang.ref.WeakReference
|
|||||||
actual object UtilsLibrary {
|
actual object UtilsLibrary {
|
||||||
private var init = false
|
private var init = false
|
||||||
private lateinit var _context: WeakReference<Context>
|
private lateinit var _context: WeakReference<Context>
|
||||||
internal val context get() = _context.get()!!
|
val context get() = _context.get()!!
|
||||||
|
|
||||||
actual fun init(vararg args: Any) {
|
actual fun init(vararg args: Any) {
|
||||||
_context = WeakReference(args[0] as Context)
|
if (!init) {
|
||||||
|
_context = WeakReference(args[0] as Context)
|
||||||
|
init = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user