Настройки

This commit is contained in:
Халимов Рустам
2026-04-14 11:46:55 +03:00
parent 3310a3c4a4
commit 58bbdae26c
14 changed files with 311 additions and 96 deletions
Binary file not shown.
Binary file not shown.
@@ -5,18 +5,23 @@ import auth.data.remote.dto.AuthResponse
import core.domain.model.ServerConfigModel import core.domain.model.ServerConfigModel
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST import retrofit2.http.POST
interface AuthApi { interface AuthApi {
@POST("auth/login") @POST("auth/login")
@Headers("Cache-Control: no-cache")
suspend fun login(@Body request: AuthRequest): AuthResponse suspend fun login(@Body request: AuthRequest): AuthResponse
@POST("auth/register") @POST("auth/register")
@Headers("Cache-Control: no-cache")
suspend fun register(@Body request: AuthRequest): AuthResponse suspend fun register(@Body request: AuthRequest): AuthResponse
@GET("config") @GET("config")
@Headers("Cache-Control: no-cache")
suspend fun getConfig(): ServerConfigModel suspend fun getConfig(): ServerConfigModel
@POST("auth/push-token") @POST("auth/push-token")
@Headers("Cache-Control: no-cache")
suspend fun updatePushToken(@Body token: String): Unit suspend fun updatePushToken(@Body token: String): Unit
} }
@@ -10,7 +10,9 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chats.presentation.components.ChatItem import chats.presentation.components.ChatItem
import stories.presentation.StoryViewModel import stories.presentation.StoryViewModel
@@ -44,9 +46,10 @@ fun ChatListScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.chats_title)) }, title = { Text(stringResource(R.string.chats_title), fontWeight = FontWeight.Bold) },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer containerColor = Color.Transparent,
titleContentColor = Color.White
) )
) )
} }
@@ -3,18 +3,55 @@ package core.domain.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class ServerConfigModel( data class ServerConfigModel(
@SerializedName("features") val features: FeaturesConfig = FeaturesConfig(), @SerializedName("stories") val stories: StoriesConfig = StoriesConfig(),
@SerializedName("limits") val limits: LimitsConfig = LimitsConfig() @SerializedName("messages") val messages: MessagesConfig = MessagesConfig(),
@SerializedName("chats") val chats: ChatsConfig = ChatsConfig(),
@SerializedName("webRtc") val webRtc: WebRtcConfig = WebRtcConfig()
) {
// Helper to keep the presentation layer simple
val features: FeaturesConfig
get() = FeaturesConfig(
stories = stories.enabled,
polls = messages.allowPolls,
calls = webRtc.enabled && webRtc.enableVoiceCalls,
groups = true // Static for now as chats config doesn't have a simple toggle
)
val limits: LimitsConfig
get() = LimitsConfig(
maxFileSize = messages.maxFileSize,
maxGroupMembers = chats.maxGroupParticipants
)
}
data class StoriesConfig(
@SerializedName("enabled") val enabled: Boolean = true
) )
data class MessagesConfig(
@SerializedName("maxFileSize") val maxFileSize: Long = 100 * 1024 * 1024,
@SerializedName("allowPolls") val allowPolls: Boolean = true
)
data class ChatsConfig(
@SerializedName("maxGroupParticipants") val maxGroupParticipants: Int = 200
)
data class WebRtcConfig(
@SerializedName("enabled") val enabled: Boolean = true,
@SerializedName("enableVoiceCalls") val enableVoiceCalls: Boolean = true,
@SerializedName("enableVideoCalls") val enableVideoCalls: Boolean = true
)
// DTOs to maintain compatibility with existing UI code if possible
data class FeaturesConfig( data class FeaturesConfig(
@SerializedName("stories") val stories: Boolean = true, val stories: Boolean,
@SerializedName("polls") val polls: Boolean = true, val polls: Boolean,
@SerializedName("calls") val calls: Boolean = true, val calls: Boolean,
@SerializedName("groups") val groups: Boolean = true val groups: Boolean
) )
data class LimitsConfig( data class LimitsConfig(
@SerializedName("maxFileSize") val maxFileSize: Long = 100 * 1024 * 1024, // 100MB default val maxFileSize: Long,
@SerializedName("maxGroupMembers") val maxGroupMembers: Int = 200 val maxGroupMembers: Int
) )
+2 -1
View File
@@ -31,7 +31,8 @@ class ServerConfig @Inject constructor(
} }
fun saveServerConfig(config: ServerConfigModel) { fun saveServerConfig(config: ServerConfigModel) {
prefs.edit().putString(KEY_CONFIG_DATA, gson.toJson(config)).apply() android.util.Log.d("ServerConfig", "Saving new config: $config")
prefs.edit().putString(KEY_CONFIG_DATA, gson.toJson(config)).commit()
} }
fun getServerConfig(): ServerConfigModel { fun getServerConfig(): ServerConfigModel {
@@ -7,13 +7,14 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChatBubble import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.filled.QuestionAnswer
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
@@ -35,25 +36,13 @@ fun GlassNavigationBar(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp) .padding(horizontal = 16.dp, vertical = 12.dp)
.height(72.dp) .height(64.dp)
.clip(RoundedCornerShape(32.dp)) .clip(RoundedCornerShape(32.dp))
.background( .background(Color(0xFF1C1C1E).copy(alpha = 0.95f))
Brush.verticalGradient(
colors = listOf(
MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
)
)
)
.border( .border(
1.dp, 0.5.dp,
Brush.verticalGradient( Color.White.copy(alpha = 0.1f),
colors = listOf(
Color.White.copy(alpha = 0.2f),
Color.Transparent
)
),
RoundedCornerShape(32.dp) RoundedCornerShape(32.dp)
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -64,13 +53,13 @@ fun GlassNavigationBar(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
NavItem( NavItem(
icon = Icons.Default.ChatBubble, icon = Icons.Default.QuestionAnswer,
label = stringResource(R.string.chats), label = stringResource(R.string.chats),
isSelected = currentRoute == "chat_list", isSelected = currentRoute == "chat_list",
onClick = { onNavigate("chat_list") } onClick = { onNavigate("chat_list") }
) )
NavItem( NavItem(
icon = Icons.Default.People, icon = Icons.Default.AccountCircle,
label = stringResource(R.string.contacts_tab), label = stringResource(R.string.contacts_tab),
isSelected = currentRoute == "contacts", isSelected = currentRoute == "contacts",
onClick = { onNavigate("contacts") } onClick = { onNavigate("contacts") }
@@ -93,62 +82,70 @@ fun GlassNavigationBar(
} }
@Composable @Composable
private fun NavItem( private fun RowScope.NavItem(
icon: ImageVector, icon: ImageVector,
label: String, label: String,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val contentColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant val contentColor = if (isSelected) Color(0xFF3390EC) else Color(0xFF94A3B8)
val backgroundColor = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else Color.Transparent
Column( Column(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(20.dp)) .weight(1f)
.background(backgroundColor)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(horizontal = 12.dp, vertical = 8.dp), .padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Icon(icon, contentDescription = label, tint = contentColor, modifier = Modifier.size(24.dp)) Icon(
Text(text = label, color = contentColor, fontSize = 10.sp, modifier = Modifier.padding(top = 2.dp)) icon,
contentDescription = label,
tint = contentColor,
modifier = Modifier.size(26.dp)
)
Text(
text = label,
color = contentColor,
fontSize = 10.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
modifier = Modifier.padding(top = 2.dp)
)
} }
} }
@Composable @Composable
private fun ProfileNavItem( private fun RowScope.ProfileNavItem(
avatarUrl: String?, avatarUrl: String?,
username: String, username: String,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent val contentColor = if (isSelected) Color(0xFF3390EC) else Color(0xFF94A3B8)
Column( Column(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(20.dp)) .weight(1f)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(horizontal = 12.dp, vertical = 8.dp), .padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(28.dp) .size(26.dp)
.border(2.dp, borderColor, CircleShape) .clip(CircleShape)
.padding(2.dp)
) { ) {
AppAvatar( AppAvatar(
url = avatarUrl, url = avatarUrl,
name = username, name = username,
size = 24.dp size = 26.dp
) )
} }
Text( Text(
text = stringResource(R.string.profile_tab), text = stringResource(R.string.profile_tab),
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, color = contentColor,
fontSize = 10.sp, fontSize = 10.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
modifier = Modifier.padding(top = 2.dp) modifier = Modifier.padding(top = 2.dp)
) )
} }
@@ -1,14 +1,24 @@
package core.presentation.settings package core.presentation.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
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.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import ru.knot.messager.R import ru.knot.messager.R
@@ -22,20 +32,38 @@ fun SettingsScreen(
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
var baseUrl by remember(state.baseUrl) { mutableStateOf(state.baseUrl) } var baseUrl by remember(state.baseUrl) { mutableStateOf(state.baseUrl) }
val lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = androidx.lifecycle.LifecycleEventObserver { _, event ->
if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) {
viewModel.loadConfig()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Scaffold( Scaffold(
containerColor = Color(0xFF0F0F10),
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.settings)) }, title = { Text(stringResource(R.string.settings), fontWeight = FontWeight.Bold) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(R.string.back)) Icon(Icons.Default.ArrowBack, contentDescription = stringResource(R.string.back), tint = Color.White)
} }
}, },
actions = { actions = {
IconButton(onClick = { viewModel.saveBaseUrl(baseUrl) }) { IconButton(onClick = { viewModel.loadConfig() }) {
Icon(Icons.Default.Save, contentDescription = stringResource(R.string.save)) Icon(Icons.Default.Sync, contentDescription = "Refresh", tint = Color.White)
}
} }
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF0F0F10),
titleContentColor = Color.White
)
) )
} }
) { paddingValues -> ) { paddingValues ->
@@ -43,60 +71,181 @@ fun SettingsScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(16.dp) .padding(16.dp)
) { ) {
Text( if (state.isLoading) {
text = stringResource(R.string.server_connection), LinearProgressIndicator(
style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
modifier = Modifier.padding(bottom = 8.dp) color = Color(0xFF3390EC)
) )
}
OutlinedTextField( if (state.error != null) {
Text(
text = "Ошибка обновления: ${state.error}",
color = Color.Red,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(bottom = 16.dp)
)
}
// Server Section
SettingsSection(title = stringResource(R.string.server_connection)) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = stringResource(R.string.api_base_url),
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF3390EC)
)
Row(verticalAlignment = Alignment.CenterVertically) {
TextField(
value = baseUrl, value = baseUrl,
onValueChange = { baseUrl = it }, onValueChange = { baseUrl = it },
label = { Text(stringResource(R.string.api_base_url)) }, modifier = Modifier.weight(1f),
modifier = Modifier.fillMaxWidth(), colors = TextFieldDefaults.colors(
placeholder = { Text("https://example.com/api/") } focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedIndicatorColor = Color(0xFF3390EC),
unfocusedIndicatorColor = Color.Gray.copy(alpha = 0.5f)
),
placeholder = { Text("https://...", color = Color.Gray) }
) )
IconButton(onClick = { viewModel.saveBaseUrl(baseUrl) }) {
Icon(Icons.Default.Save, contentDescription = null, tint = Color(0xFF3390EC))
}
}
}
}
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( // Features Section
text = stringResource(R.string.server_features), SettingsSection(title = stringResource(R.string.server_features)) {
style = MaterialTheme.typography.titleMedium, Column {
modifier = Modifier.padding(bottom = 8.dp) SettingsToggleItem(
icon = Icons.Default.History,
label = stringResource(R.string.stories),
enabled = state.config.features.stories
) )
SettingsToggleItem(
FeatureStatusItem(stringResource(R.string.stories), state.config.features.stories) icon = Icons.Default.Poll,
FeatureStatusItem(stringResource(R.string.polls), state.config.features.polls) label = stringResource(R.string.polls),
FeatureStatusItem(stringResource(R.string.calls), state.config.features.calls) enabled = state.config.features.polls
FeatureStatusItem(stringResource(R.string.groups), state.config.features.groups)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.limits),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text("${stringResource(R.string.max_file_size)}: ${state.config.limits.maxFileSize / (1024 * 1024)} MB") SettingsToggleItem(
Text("${stringResource(R.string.max_group_members)}: ${state.config.limits.maxGroupMembers}") icon = Icons.Default.Call,
label = stringResource(R.string.calls),
enabled = state.config.features.calls
)
SettingsToggleItem(
icon = Icons.Default.Group,
label = stringResource(R.string.groups),
enabled = state.config.features.groups
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Limits Section
SettingsSection(title = stringResource(R.string.limits)) {
Column {
SettingsInfoItem(
icon = Icons.Default.Storage,
label = stringResource(R.string.max_file_size),
value = "${state.config.limits.maxFileSize / (1024 * 1024)} MB"
)
SettingsInfoItem(
icon = Icons.Default.People,
label = stringResource(R.string.max_group_members),
value = state.config.limits.maxGroupMembers.toString()
)
}
}
Spacer(modifier = Modifier.height(32.dp))
// Logout Button
Button(
onClick = onLogout,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE53935)),
shape = RoundedCornerShape(12.dp)
) {
Icon(Icons.Default.Logout, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.log_out), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(80.dp)) // Space for bottom bar
} }
} }
} }
@Composable @Composable
fun FeatureStatusItem(name: String, enabled: Boolean) { fun SettingsSection(title: String, content: @Composable () -> Unit) {
Column {
Text(
text = title.uppercase(),
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF3390EC),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
)
Surface(
color = Color(0xFF1C1C1E),
shape = RoundedCornerShape(16.dp),
modifier = Modifier.fillMaxWidth()
) {
content()
}
}
}
@Composable
fun SettingsToggleItem(icon: ImageVector, label: String, enabled: Boolean) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text(name) Row(verticalAlignment = Alignment.CenterVertically) {
Icon(icon, contentDescription = null, tint = Color.Gray, modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(16.dp))
Text(label, color = Color.White, fontSize = 16.sp)
}
Text( Text(
text = if (enabled) stringResource(R.string.enabled) else stringResource(R.string.disabled), text = if (enabled) stringResource(R.string.enabled) else stringResource(R.string.disabled),
color = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error color = if (enabled) Color(0xFF3390EC) else Color(0xFFE53935),
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
}
}
@Composable
fun SettingsInfoItem(icon: ImageVector, label: String, value: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(icon, contentDescription = null, tint = Color.Gray, modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(16.dp))
Text(label, color = Color.White, fontSize = 16.sp)
}
Text(
text = value,
color = Color.White,
fontWeight = FontWeight.Medium,
fontSize = 14.sp
) )
} }
} }
@@ -1,5 +1,6 @@
package core.presentation.settings package core.presentation.settings
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import core.domain.model.ServerConfigModel import core.domain.model.ServerConfigModel
@@ -14,18 +15,26 @@ import javax.inject.Inject
data class SettingsState( data class SettingsState(
val baseUrl: String = "", val baseUrl: String = "",
val config: ServerConfigModel = ServerConfigModel() val config: ServerConfigModel = ServerConfigModel(),
val isLoading: Boolean = false,
val error: String? = null
) )
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val serverConfig: ServerConfig private val serverConfig: ServerConfig,
private val authRepository: auth.domain.repository.AuthRepository
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(SettingsState()) private val _state = MutableStateFlow(SettingsState())
val state: StateFlow<SettingsState> = _state.asStateFlow() val state: StateFlow<SettingsState> = _state.asStateFlow()
init { init {
refreshState()
loadConfig()
}
private fun refreshState() {
_state.update { _state.update {
it.copy( it.copy(
baseUrl = serverConfig.getBaseUrl(), baseUrl = serverConfig.getBaseUrl(),
@@ -34,6 +43,20 @@ class SettingsViewModel @Inject constructor(
} }
} }
fun loadConfig() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
val result = authRepository.fetchConfig()
Log.d("SettingsViewModel", "Fetch result: ${result.isSuccess}")
if (result.isSuccess) {
refreshState()
} else {
_state.update { it.copy(error = result.exceptionOrNull()?.message) }
}
_state.update { it.copy(isLoading = false) }
}
}
fun saveBaseUrl(url: String) { fun saveBaseUrl(url: String) {
serverConfig.setBaseUrl(url) serverConfig.setBaseUrl(url)
_state.update { it.copy(baseUrl = url) } _state.update { it.copy(baseUrl = url) }