Настройки
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
Reference in New Issue
Block a user