diff --git a/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin b/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin index 6d6b5a9..936ef50 100644 Binary files a/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin and b/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin differ diff --git a/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock b/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock index 70d3f74..397a875 100644 Binary files a/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock and b/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock differ diff --git a/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin b/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin index a3016be..dd98604 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin and b/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin differ diff --git a/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock b/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock index 6d426be..ad3f864 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock and b/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock differ diff --git a/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin b/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin index d6a1389..58d92ea 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin and b/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin differ diff --git a/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 834a9ae..05f79fa 100644 Binary files a/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin b/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin index ead1509..db8dd4b 100644 Binary files a/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin and b/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/client-mobile/auth/data/remote/api/AuthApi.kt b/client-mobile/auth/data/remote/api/AuthApi.kt index 14627fa..9a68f1e 100644 --- a/client-mobile/auth/data/remote/api/AuthApi.kt +++ b/client-mobile/auth/data/remote/api/AuthApi.kt @@ -5,18 +5,23 @@ import auth.data.remote.dto.AuthResponse import core.domain.model.ServerConfigModel import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.Headers import retrofit2.http.POST interface AuthApi { @POST("auth/login") + @Headers("Cache-Control: no-cache") suspend fun login(@Body request: AuthRequest): AuthResponse @POST("auth/register") + @Headers("Cache-Control: no-cache") suspend fun register(@Body request: AuthRequest): AuthResponse @GET("config") + @Headers("Cache-Control: no-cache") suspend fun getConfig(): ServerConfigModel @POST("auth/push-token") + @Headers("Cache-Control: no-cache") suspend fun updatePushToken(@Body token: String): Unit } diff --git a/client-mobile/chats/presentation/chat_list/ChatListScreen.kt b/client-mobile/chats/presentation/chat_list/ChatListScreen.kt index f4cd081..4d4eb71 100644 --- a/client-mobile/chats/presentation/chat_list/ChatListScreen.kt +++ b/client-mobile/chats/presentation/chat_list/ChatListScreen.kt @@ -10,7 +10,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import chats.presentation.components.ChatItem import stories.presentation.StoryViewModel @@ -44,9 +46,10 @@ fun ChatListScreen( Scaffold( topBar = { TopAppBar( - title = { Text(stringResource(R.string.chats_title)) }, + title = { Text(stringResource(R.string.chats_title), fontWeight = FontWeight.Bold) }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer + containerColor = Color.Transparent, + titleContentColor = Color.White ) ) } diff --git a/client-mobile/core/domain/model/ServerConfigModel.kt b/client-mobile/core/domain/model/ServerConfigModel.kt index d327d0d..c8631a5 100644 --- a/client-mobile/core/domain/model/ServerConfigModel.kt +++ b/client-mobile/core/domain/model/ServerConfigModel.kt @@ -3,18 +3,55 @@ package core.domain.model import com.google.gson.annotations.SerializedName data class ServerConfigModel( - @SerializedName("features") val features: FeaturesConfig = FeaturesConfig(), - @SerializedName("limits") val limits: LimitsConfig = LimitsConfig() + @SerializedName("stories") val stories: StoriesConfig = StoriesConfig(), + @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( - @SerializedName("stories") val stories: Boolean = true, - @SerializedName("polls") val polls: Boolean = true, - @SerializedName("calls") val calls: Boolean = true, - @SerializedName("groups") val groups: Boolean = true + val stories: Boolean, + val polls: Boolean, + val calls: Boolean, + val groups: Boolean ) data class LimitsConfig( - @SerializedName("maxFileSize") val maxFileSize: Long = 100 * 1024 * 1024, // 100MB default - @SerializedName("maxGroupMembers") val maxGroupMembers: Int = 200 + val maxFileSize: Long, + val maxGroupMembers: Int ) diff --git a/client-mobile/core/network/ServerConfig.kt b/client-mobile/core/network/ServerConfig.kt index ded9527..f0d7941 100644 --- a/client-mobile/core/network/ServerConfig.kt +++ b/client-mobile/core/network/ServerConfig.kt @@ -31,7 +31,8 @@ class ServerConfig @Inject constructor( } 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 { diff --git a/client-mobile/core/presentation/components/GlassNavigationBar.kt b/client-mobile/core/presentation/components/GlassNavigationBar.kt index ca2b9d9..a319d5f 100644 --- a/client-mobile/core/presentation/components/GlassNavigationBar.kt +++ b/client-mobile/core/presentation/components/GlassNavigationBar.kt @@ -7,13 +7,14 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ChatBubble -import androidx.compose.material.icons.filled.People +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.QuestionAnswer import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush @@ -35,25 +36,13 @@ fun GlassNavigationBar( Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 16.dp) - .height(72.dp) + .padding(horizontal = 16.dp, vertical = 12.dp) + .height(64.dp) .clip(RoundedCornerShape(32.dp)) - .background( - Brush.verticalGradient( - colors = listOf( - MaterialTheme.colorScheme.surface.copy(alpha = 0.7f), - MaterialTheme.colorScheme.surface.copy(alpha = 0.5f) - ) - ) - ) + .background(Color(0xFF1C1C1E).copy(alpha = 0.95f)) .border( - 1.dp, - Brush.verticalGradient( - colors = listOf( - Color.White.copy(alpha = 0.2f), - Color.Transparent - ) - ), + 0.5.dp, + Color.White.copy(alpha = 0.1f), RoundedCornerShape(32.dp) ), contentAlignment = Alignment.Center @@ -64,13 +53,13 @@ fun GlassNavigationBar( verticalAlignment = Alignment.CenterVertically ) { NavItem( - icon = Icons.Default.ChatBubble, + icon = Icons.Default.QuestionAnswer, label = stringResource(R.string.chats), isSelected = currentRoute == "chat_list", onClick = { onNavigate("chat_list") } ) NavItem( - icon = Icons.Default.People, + icon = Icons.Default.AccountCircle, label = stringResource(R.string.contacts_tab), isSelected = currentRoute == "contacts", onClick = { onNavigate("contacts") } @@ -93,62 +82,70 @@ fun GlassNavigationBar( } @Composable -private fun NavItem( +private fun RowScope.NavItem( icon: ImageVector, label: String, isSelected: Boolean, onClick: () -> Unit ) { - val contentColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant - val backgroundColor = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) else Color.Transparent - + val contentColor = if (isSelected) Color(0xFF3390EC) else Color(0xFF94A3B8) Column( modifier = Modifier - .clip(RoundedCornerShape(20.dp)) - .background(backgroundColor) + .weight(1f) .clickable(onClick = onClick) - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(vertical = 4.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Icon(icon, contentDescription = label, tint = contentColor, modifier = Modifier.size(24.dp)) - Text(text = label, color = contentColor, fontSize = 10.sp, modifier = Modifier.padding(top = 2.dp)) + Icon( + 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 -private fun ProfileNavItem( +private fun RowScope.ProfileNavItem( avatarUrl: String?, username: String, isSelected: Boolean, onClick: () -> Unit ) { - val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent + val contentColor = if (isSelected) Color(0xFF3390EC) else Color(0xFF94A3B8) Column( modifier = Modifier - .clip(RoundedCornerShape(20.dp)) + .weight(1f) .clickable(onClick = onClick) - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(vertical = 4.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Box( modifier = Modifier - .size(28.dp) - .border(2.dp, borderColor, CircleShape) - .padding(2.dp) + .size(26.dp) + .clip(CircleShape) ) { AppAvatar( url = avatarUrl, name = username, - size = 24.dp + size = 26.dp ) } Text( text = stringResource(R.string.profile_tab), - color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + color = contentColor, fontSize = 10.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, modifier = Modifier.padding(top = 2.dp) ) } diff --git a/client-mobile/core/presentation/settings/SettingsScreen.kt b/client-mobile/core/presentation/settings/SettingsScreen.kt index 3216593..5fe80ae 100644 --- a/client-mobile/core/presentation/settings/SettingsScreen.kt +++ b/client-mobile/core/presentation/settings/SettingsScreen.kt @@ -1,14 +1,24 @@ package core.presentation.settings +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable 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.filled.ArrowBack -import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment 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.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import ru.knot.messager.R @@ -22,20 +32,38 @@ fun SettingsScreen( val state by viewModel.state.collectAsState() 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( + containerColor = Color(0xFF0F0F10), topBar = { TopAppBar( - title = { Text(stringResource(R.string.settings)) }, + title = { Text(stringResource(R.string.settings), fontWeight = FontWeight.Bold) }, navigationIcon = { 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 = { - IconButton(onClick = { viewModel.saveBaseUrl(baseUrl) }) { - Icon(Icons.Default.Save, contentDescription = stringResource(R.string.save)) + IconButton(onClick = { viewModel.loadConfig() }) { + Icon(Icons.Default.Sync, contentDescription = "Refresh", tint = Color.White) } - } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color(0xFF0F0F10), + titleContentColor = Color.White + ) ) } ) { paddingValues -> @@ -43,60 +71,181 @@ fun SettingsScreen( modifier = Modifier .fillMaxSize() .padding(paddingValues) + .verticalScroll(rememberScrollState()) .padding(16.dp) ) { - Text( - text = stringResource(R.string.server_connection), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(bottom = 8.dp) - ) - - OutlinedTextField( - value = baseUrl, - onValueChange = { baseUrl = it }, - label = { Text(stringResource(R.string.api_base_url)) }, - modifier = Modifier.fillMaxWidth(), - placeholder = { Text("https://example.com/api/") } - ) + if (state.isLoading) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + color = Color(0xFF3390EC) + ) + } + 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, + onValueChange = { baseUrl = it }, + modifier = Modifier.weight(1f), + colors = TextFieldDefaults.colors( + 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)) + + // Features Section + SettingsSection(title = stringResource(R.string.server_features)) { + Column { + SettingsToggleItem( + icon = Icons.Default.History, + label = stringResource(R.string.stories), + enabled = state.config.features.stories + ) + SettingsToggleItem( + icon = Icons.Default.Poll, + label = stringResource(R.string.polls), + enabled = state.config.features.polls + ) + SettingsToggleItem( + 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) + } - Text( - text = stringResource(R.string.server_features), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(bottom = 8.dp) - ) - - FeatureStatusItem(stringResource(R.string.stories), state.config.features.stories) - FeatureStatusItem(stringResource(R.string.polls), state.config.features.polls) - FeatureStatusItem(stringResource(R.string.calls), state.config.features.calls) - 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") - Text("${stringResource(R.string.max_group_members)}: ${state.config.limits.maxGroupMembers}") + Spacer(modifier = Modifier.height(80.dp)) // Space for bottom bar } } } @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( modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp), + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, 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 = 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 ) } } diff --git a/client-mobile/core/presentation/settings/SettingsViewModel.kt b/client-mobile/core/presentation/settings/SettingsViewModel.kt index 7a4a526..436b48c 100644 --- a/client-mobile/core/presentation/settings/SettingsViewModel.kt +++ b/client-mobile/core/presentation/settings/SettingsViewModel.kt @@ -1,5 +1,6 @@ package core.presentation.settings +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import core.domain.model.ServerConfigModel @@ -14,18 +15,26 @@ import javax.inject.Inject data class SettingsState( val baseUrl: String = "", - val config: ServerConfigModel = ServerConfigModel() + val config: ServerConfigModel = ServerConfigModel(), + val isLoading: Boolean = false, + val error: String? = null ) @HiltViewModel class SettingsViewModel @Inject constructor( - private val serverConfig: ServerConfig + private val serverConfig: ServerConfig, + private val authRepository: auth.domain.repository.AuthRepository ) : ViewModel() { private val _state = MutableStateFlow(SettingsState()) val state: StateFlow = _state.asStateFlow() init { + refreshState() + loadConfig() + } + + private fun refreshState() { _state.update { it.copy( 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) { serverConfig.setBaseUrl(url) _state.update { it.copy(baseUrl = url) }