refactor: Clean up OnboardingScreen code for improved readability and maintainability

This commit is contained in:
k1ngsterr1
2026-01-20 03:06:06 +05:00
parent 6b1c84a7bc
commit 0c4c636823
4 changed files with 1327 additions and 580 deletions

View File

@@ -39,6 +39,7 @@ import com.rosetta.messenger.ui.chats.ChatsListScreen
import com.rosetta.messenger.ui.chats.SearchScreen
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
import com.rosetta.messenger.ui.settings.SettingsScreen
import com.rosetta.messenger.ui.splash.SplashScreen
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
import java.text.SimpleDateFormat
@@ -437,15 +438,18 @@ fun MainScreen(
// Навигация между экранами
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
var showSearchScreen by remember { mutableStateOf(false) }
var showSettingsScreen by remember { mutableStateOf(false) }
// 🔥 TELEGRAM-STYLE анимация - чистый slide БЕЗ прозрачности
AnimatedContent(
targetState = Triple(selectedUser, showSearchScreen, Unit),
targetState = Triple(selectedUser, showSearchScreen, showSettingsScreen),
transitionSpec = {
val isEnteringChat = targetState.first != null && initialState.first == null
val isExitingChat = targetState.first == null && initialState.first != null
val isEnteringSearch = targetState.second && !initialState.second
val isExitingSearch = !targetState.second && initialState.second
val isEnteringSettings = targetState.third && !initialState.third
val isExitingSettings = !targetState.third && initialState.third
when {
// 🚀 Вход в чат - плавный fade
@@ -478,6 +482,18 @@ fun MainScreen(
fadeOut(animationSpec = tween(150))
}
// ⚙️ Вход в Settings - плавный fade
isEnteringSettings -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
}
// 🔙 Выход из Settings - плавный fade
isExitingSettings -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
}
// Default - мгновенный переход
else -> {
EnterTransition.None togetherWith ExitTransition.None
@@ -485,7 +501,7 @@ fun MainScreen(
}
},
label = "screenNavigation"
) { (currentUser, isSearchOpen, _) ->
) { (currentUser, isSearchOpen, isSettingsOpen) ->
when {
currentUser != null -> {
// Экран чата
@@ -525,6 +541,41 @@ fun MainScreen(
}
)
}
isSettingsOpen -> {
// Экран настроек
SettingsScreen(
isDarkTheme = isDarkTheme,
accountName = accountName,
accountPhone = accountPhone,
accountPublicKey = accountPublicKey,
onBack = { showSettingsScreen = false },
onToggleTheme = onToggleTheme,
onProfileClick = {
// TODO: Navigate to profile editor
},
onPrivacySecurityClick = {
// TODO: Navigate to privacy settings
},
onNotificationsClick = {
// TODO: Navigate to notifications settings
},
onDataStorageClick = {
// TODO: Navigate to data storage settings
},
onChatSettingsClick = {
// TODO: Navigate to chat settings
},
onLanguageClick = {
// TODO: Navigate to language selection
},
onHelpClick = {
// TODO: Navigate to help center
},
onAboutClick = {
// TODO: Show about dialog
}
)
}
else -> {
// Список чатов
ChatsListScreen(
@@ -558,9 +609,7 @@ fun MainScreen(
online = 1
)
},
onSettingsClick = {
// TODO: Navigate to settings
},
onSettingsClick = { showSettingsScreen = true },
onInviteFriendsClick = {
// TODO: Share invite link
},

View File

@@ -5,38 +5,178 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "rosetta_preferences")
private val Context.dataStore: DataStore<Preferences> by
preferencesDataStore(name = "rosetta_preferences")
class PreferencesManager(private val context: Context) {
companion object {
// Onboarding & Theme
val HAS_SEEN_ONBOARDING = booleanPreferencesKey("has_seen_onboarding")
val IS_DARK_THEME = booleanPreferencesKey("is_dark_theme")
// Notifications
val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
val NOTIFICATION_SOUND_ENABLED = booleanPreferencesKey("notification_sound_enabled")
val NOTIFICATION_VIBRATE_ENABLED = booleanPreferencesKey("notification_vibrate_enabled")
val NOTIFICATION_PREVIEW_ENABLED = booleanPreferencesKey("notification_preview_enabled")
// Chat Settings
val MESSAGE_TEXT_SIZE = intPreferencesKey("message_text_size") // 0=small, 1=medium, 2=large
val SEND_BY_ENTER = booleanPreferencesKey("send_by_enter")
val AUTO_DOWNLOAD_PHOTOS = booleanPreferencesKey("auto_download_photos")
val AUTO_DOWNLOAD_VIDEOS = booleanPreferencesKey("auto_download_videos")
val AUTO_DOWNLOAD_FILES = booleanPreferencesKey("auto_download_files")
// Privacy
val SHOW_ONLINE_STATUS = booleanPreferencesKey("show_online_status")
val SHOW_READ_RECEIPTS = booleanPreferencesKey("show_read_receipts")
val SHOW_TYPING_INDICATOR = booleanPreferencesKey("show_typing_indicator")
// Language
val APP_LANGUAGE = stringPreferencesKey("app_language") // "en", "ru", etc.
}
val hasSeenOnboarding: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[HAS_SEEN_ONBOARDING] ?: false
}
// ═════════════════════════════════════════════════════════════
// 🎨 ONBOARDING & THEME
// ═════════════════════════════════════════════════════════════
val isDarkTheme: Flow<Boolean> = context.dataStore.data
.map { preferences ->
val hasSeenOnboarding: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[HAS_SEEN_ONBOARDING] ?: false }
val isDarkTheme: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[IS_DARK_THEME] ?: true // Default to dark theme like Telegram
}
suspend fun setHasSeenOnboarding(value: Boolean) {
context.dataStore.edit { preferences ->
preferences[HAS_SEEN_ONBOARDING] = value
}
context.dataStore.edit { preferences -> preferences[HAS_SEEN_ONBOARDING] = value }
}
suspend fun setDarkTheme(value: Boolean) {
context.dataStore.edit { preferences ->
preferences[IS_DARK_THEME] = value
}
context.dataStore.edit { preferences -> preferences[IS_DARK_THEME] = value }
}
// ═════════════════════════════════════════════════════════════
// 🔔 NOTIFICATIONS
// ═════════════════════════════════════════════════════════════
val notificationsEnabled: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[NOTIFICATIONS_ENABLED] ?: true }
val notificationSoundEnabled: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[NOTIFICATION_SOUND_ENABLED] ?: true
}
val notificationVibrateEnabled: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[NOTIFICATION_VIBRATE_ENABLED] ?: true
}
val notificationPreviewEnabled: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[NOTIFICATION_PREVIEW_ENABLED] ?: true
}
suspend fun setNotificationsEnabled(value: Boolean) {
context.dataStore.edit { preferences -> preferences[NOTIFICATIONS_ENABLED] = value }
}
suspend fun setNotificationSoundEnabled(value: Boolean) {
context.dataStore.edit { preferences -> preferences[NOTIFICATION_SOUND_ENABLED] = value }
}
suspend fun setNotificationVibrateEnabled(value: Boolean) {
context.dataStore.edit { preferences -> preferences[NOTIFICATION_VIBRATE_ENABLED] = value }
}
suspend fun setNotificationPreviewEnabled(value: Boolean) {
context.dataStore.edit { preferences -> preferences[NOTIFICATION_PREVIEW_ENABLED] = value }
}
// ═════════════════════════════════════════════════════════════
// 💬 CHAT SETTINGS
// ═════════════════════════════════════════════════════════════
val messageTextSize: Flow<Int> =
context.dataStore.data.map { preferences ->
preferences[MESSAGE_TEXT_SIZE] ?: 1 // Default medium
}
val sendByEnter: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[SEND_BY_ENTER] ?: false }
val autoDownloadPhotos: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_PHOTOS] ?: true }
val autoDownloadVideos: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_VIDEOS] ?: false }
val autoDownloadFiles: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_FILES] ?: false }
suspend fun setMessageTextSize(value: Int) {
context.dataStore.edit { preferences -> preferences[MESSAGE_TEXT_SIZE] = value }
}
suspend fun setSendByEnter(value: Boolean) {
context.dataStore.edit { preferences -> preferences[SEND_BY_ENTER] = value }
}
suspend fun setAutoDownloadPhotos(value: Boolean) {
context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_PHOTOS] = value }
}
suspend fun setAutoDownloadVideos(value: Boolean) {
context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_VIDEOS] = value }
}
suspend fun setAutoDownloadFiles(value: Boolean) {
context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_FILES] = value }
}
// ═════════════════════════════════════════════════════════════
// 🔐 PRIVACY
// ═════════════════════════════════════════════════════════════
val showOnlineStatus: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[SHOW_ONLINE_STATUS] ?: true }
val showReadReceipts: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[SHOW_READ_RECEIPTS] ?: true }
val showTypingIndicator: Flow<Boolean> =
context.dataStore.data.map { preferences -> preferences[SHOW_TYPING_INDICATOR] ?: true }
suspend fun setShowOnlineStatus(value: Boolean) {
context.dataStore.edit { preferences -> preferences[SHOW_ONLINE_STATUS] = value }
}
suspend fun setShowReadReceipts(value: Boolean) {
context.dataStore.edit { preferences -> preferences[SHOW_READ_RECEIPTS] = value }
}
suspend fun setShowTypingIndicator(value: Boolean) {
context.dataStore.edit { preferences -> preferences[SHOW_TYPING_INDICATOR] = value }
}
// ═════════════════════════════════════════════════════════════
// 🌐 LANGUAGE
// ═════════════════════════════════════════════════════════════
val appLanguage: Flow<String> =
context.dataStore.data.map { preferences ->
preferences[APP_LANGUAGE] ?: "en" // Default English
}
suspend fun setAppLanguage(value: String) {
context.dataStore.edit { preferences -> preferences[APP_LANGUAGE] = value }
}
}

View File

@@ -129,7 +129,11 @@ fun OnboardingScreen(
val b = (b1 + (b2 - b1) * navProgress).toInt()
window.navigationBarColor =
(0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
(0xFF000000 or
(r.toLong() shl 16) or
(g.toLong() shl 8) or
b.toLong())
.toInt()
}
}
@@ -148,14 +152,16 @@ fun OnboardingScreen(
LaunchedEffect(Unit) {
if (!view.isInEditMode) {
val window = (view.context as android.app.Activity).window
window.navigationBarColor = if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt()
window.navigationBarColor =
if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt()
}
}
val backgroundColor by
animateColorAsState(
targetValue =
if (isDarkTheme) OnboardingBackground else OnboardingBackgroundLight,
if (isDarkTheme) OnboardingBackground
else OnboardingBackgroundLight,
animationSpec =
if (!hasInitialized) snap()
else tween(800, easing = FastOutSlowInEasing),
@@ -227,7 +233,8 @@ fun OnboardingScreen(
onThemeToggle()
}
},
modifier = Modifier.align(Alignment.TopEnd).padding(16.dp).statusBarsPadding()
modifier =
Modifier.align(Alignment.TopEnd).padding(16.dp).statusBarsPadding()
)
Column(
@@ -255,7 +262,8 @@ fun OnboardingScreen(
Modifier.fillMaxWidth().height(150.dp).graphicsLayer {
// Hardware acceleration for entire pager
compositingStrategy =
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
androidx.compose.ui.graphics
.CompositingStrategy.Offscreen
},
// Pre-load adjacent pages for smooth swiping
beyondBoundsPageCount = 2,
@@ -263,12 +271,19 @@ fun OnboardingScreen(
PagerDefaults.flingBehavior(
state = pagerState,
pagerSnapDistance =
PagerSnapDistance.atMost(0), // Snap to nearest page
lowVelocityAnimationSpec = snap(), // Instant!
snapAnimationSpec = snap(), // No animation!
positionalThreshold =
0.4f // Увеличен порог с 0.5 до 0.4 (нужно больше
// свайпнуть)
PagerSnapDistance.atMost(
1
), // Нужен более значительный свайп
lowVelocityAnimationSpec =
tween(
durationMillis = 400,
easing = FastOutSlowInEasing
),
snapAnimationSpec =
spring(
dampingRatio = 0.8f,
stiffness = 380f
)
)
) { page ->
OnboardingPageContent(
@@ -278,7 +293,8 @@ fun OnboardingScreen(
highlightColor = PrimaryBlue,
pageOffset =
((pagerState.currentPage - page) +
pagerState.currentPageOffsetFraction)
pagerState
.currentPageOffsetFraction)
.absoluteValue
)
}
@@ -315,14 +331,16 @@ fun ThemeToggleButton(
val rotation by
animateFloatAsState(
targetValue = if (isDarkTheme) 360f else 0f,
animationSpec = spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessLow),
animationSpec =
spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessLow),
label = "rotation"
)
val scale by
animateFloatAsState(
targetValue = 1f,
animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium),
animationSpec =
spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium),
label = "scale"
)
@@ -361,9 +379,11 @@ fun ThemeToggleButton(
) {
Icon(
imageVector =
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
if (isDarkTheme) Icons.Default.LightMode
else Icons.Default.DarkMode,
contentDescription =
if (isDarkTheme) "Switch to Light Mode" else "Switch to Dark Mode",
if (isDarkTheme) "Switch to Light Mode"
else "Switch to Dark Mode",
tint = iconColor,
modifier = Modifier.size(24.dp)
)
@@ -432,7 +452,8 @@ fun AnimatedRosettaLogo(
targetValue = 1.1f,
animationSpec =
infiniteRepeatable(
animation = tween(800, easing = FastOutSlowInEasing),
animation =
tween(800, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "pulseScale"
@@ -455,7 +476,9 @@ fun AnimatedRosettaLogo(
Modifier.size(180.dp)
.scale(if (currentPage == 0) pulseScale else 1f)
.background(
color = Color(0xFF54A9EB).copy(alpha = 0.2f),
color =
Color(0xFF54A9EB)
.copy(alpha = 0.2f),
shape = CircleShape
)
)
@@ -477,7 +500,8 @@ fun AnimatedRosettaLogo(
alpha = if (currentPage == 1) 1f else 0f
// Hardware layer optimization
compositingStrategy =
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
androidx.compose.ui.graphics
.CompositingStrategy.Offscreen
// Disable clipping for performance
clip = false
},
@@ -496,7 +520,8 @@ fun AnimatedRosettaLogo(
Modifier.fillMaxSize().graphicsLayer {
alpha = if (currentPage == 2) 1f else 0f
compositingStrategy =
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
androidx.compose.ui.graphics
.CompositingStrategy.Offscreen
clip = false
},
maintainOriginalImageBounds = true,
@@ -513,7 +538,8 @@ fun AnimatedRosettaLogo(
Modifier.fillMaxSize().graphicsLayer {
alpha = if (currentPage == 3) 1f else 0f
compositingStrategy =
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
androidx.compose.ui.graphics
.CompositingStrategy.Offscreen
clip = false
},
maintainOriginalImageBounds = true,
@@ -530,7 +556,8 @@ fun AnimatedRosettaLogo(
Modifier.fillMaxSize().graphicsLayer {
alpha = if (currentPage == 4) 1f else 0f
compositingStrategy =
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
androidx.compose.ui.graphics
.CompositingStrategy.Offscreen
clip = false
},
maintainOriginalImageBounds = true,
@@ -588,17 +615,33 @@ fun OnboardingPageContent(
// Find and highlight words
page.highlightWords.forEach { word ->
val startIndex = description.indexOf(word, currentIndex, ignoreCase = true)
val startIndex =
description.indexOf(word, currentIndex, ignoreCase = true)
if (startIndex >= 0) {
// Add text before the word
if (startIndex > currentIndex) {
withStyle(SpanStyle(color = secondaryTextColor)) {
append(description.substring(currentIndex, startIndex))
append(
description.substring(
currentIndex,
startIndex
)
)
}
}
// Add highlighted word
withStyle(SpanStyle(color = highlightColor, fontWeight = FontWeight.SemiBold)) {
append(description.substring(startIndex, startIndex + word.length))
withStyle(
SpanStyle(
color = highlightColor,
fontWeight = FontWeight.SemiBold
)
) {
append(
description.substring(
startIndex,
startIndex + word.length
)
)
}
currentIndex = startIndex + word.length
}
@@ -648,7 +691,10 @@ fun PagerIndicator(
Modifier.height(8.dp)
.width(width)
.clip(CircleShape)
.background(if (isSelected) selectedColor else unselectedColor)
.background(
if (isSelected) selectedColor
else unselectedColor
)
)
}
}
@@ -688,14 +734,21 @@ fun StartMessagingButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
Brush.linearGradient(
colors =
listOf(
Color.White.copy(alpha = 0f),
Color.White.copy(alpha = 0.3f),
Color.White.copy(alpha = 0f)
Color.White.copy(
alpha = 0f
),
Color.White.copy(
alpha = 0.3f
),
Color.White.copy(
alpha = 0f
)
),
start = Offset(shimmerStart, 0f),
end =
Offset(
shimmerStart + shimmerWidth,
shimmerStart +
shimmerWidth,
size.height
)
)

View File

@@ -0,0 +1,505 @@
package com.rosetta.messenger.ui.settings
import androidx.compose.animation.*
import androidx.compose.foundation.*
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.*
import androidx.compose.material.icons.outlined.*
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
isDarkTheme: Boolean,
accountName: String,
accountPhone: String,
accountPublicKey: String,
onBack: () -> Unit,
onToggleTheme: () -> Unit,
onProfileClick: () -> Unit = {},
onPrivacySecurityClick: () -> Unit = {},
onNotificationsClick: () -> Unit = {},
onDataStorageClick: () -> Unit = {},
onChatSettingsClick: () -> Unit = {},
onLanguageClick: () -> Unit = {},
onHelpClick: () -> Unit = {},
onAboutClick: () -> Unit = {}
) {
// Цвета в зависимости от темы
val backgroundColor = if (isDarkTheme) Color(0xFF0F0F0F) else Color.White
val surfaceColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF5F5F5)
val textColor = if (isDarkTheme) Color.White else Color(0xFF1A1A1A)
val secondaryTextColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666)
val dividerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8)
val iconTintColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666)
Column(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
// ═════════════════════════════════════════════════════════════
// 🎨 TOP BAR
// ═════════════════════════════════════════════════════════════
TopAppBar(
title = {
Text(
text = "Settings",
color = textColor,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold
)
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = textColor
)
}
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = backgroundColor)
)
// ═════════════════════════════════════════════════════════════
// 📱 CONTENT
// ═════════════════════════════════════════════════════════════
Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
// ═════════════════════════════════════════════════════════════
// 👤 PROFILE SECTION
// ═════════════════════════════════════════════════════════════
Surface(
modifier =
Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = onProfileClick),
color = surfaceColor
) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
Box(
modifier =
Modifier.size(64.dp).clip(CircleShape).background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Text(
text = getInitials(accountName),
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
Spacer(modifier = Modifier.width(16.dp))
// Name and Phone
Column(modifier = Modifier.weight(1f)) {
Text(
text = accountName,
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
)
Spacer(modifier = Modifier.height(4.dp))
Text(text = accountPhone, fontSize = 14.sp, color = secondaryTextColor)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = accountPublicKey.take(12) + "...",
fontSize = 12.sp,
color = secondaryTextColor.copy(alpha = 0.7f)
)
}
// Arrow
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = iconTintColor
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// ═════════════════════════════════════════════════════════════
// 🎨 APPEARANCE SECTION
// ═════════════════════════════════════════════════════════════
SettingsSectionTitle(title = "Appearance", isDarkTheme = isDarkTheme)
Surface(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
SettingsToggleItem(
icon =
if (isDarkTheme) Icons.Outlined.DarkMode
else Icons.Outlined.LightMode,
title = "Dark Mode",
subtitle = if (isDarkTheme) "Enabled" else "Disabled",
isChecked = isDarkTheme,
onToggle = onToggleTheme,
isDarkTheme = isDarkTheme
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// 🔐 PRIVACY & SECURITY SECTION
// ═════════════════════════════════════════════════════════════
SettingsSectionTitle(title = "Privacy & Security", isDarkTheme = isDarkTheme)
Surface(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
SettingsNavigationItem(
icon = Icons.Outlined.Lock,
title = "Privacy Settings",
subtitle = "Control who can see your info",
onClick = onPrivacySecurityClick,
isDarkTheme = isDarkTheme,
showDivider = true
)
SettingsNavigationItem(
icon = Icons.Outlined.Security,
title = "Security",
subtitle = "Passcode, 2FA, sessions",
onClick = onPrivacySecurityClick,
isDarkTheme = isDarkTheme,
showDivider = true
)
SettingsNavigationItem(
icon = Icons.Outlined.Block,
title = "Blocked Users",
subtitle = "Manage blocked contacts",
onClick = onPrivacySecurityClick,
isDarkTheme = isDarkTheme
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// 🔔 NOTIFICATIONS SECTION
// ═════════════════════════════════════════════════════════════
SettingsSectionTitle(title = "Notifications & Sounds", isDarkTheme = isDarkTheme)
Surface(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
SettingsNavigationItem(
icon = Icons.Outlined.Notifications,
title = "Notifications",
subtitle = "Messages, groups, channels",
onClick = onNotificationsClick,
isDarkTheme = isDarkTheme,
showDivider = true
)
SettingsNavigationItem(
icon = Icons.Outlined.VolumeUp,
title = "Sounds",
subtitle = "Notification sounds",
onClick = onNotificationsClick,
isDarkTheme = isDarkTheme
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// 💾 DATA & STORAGE SECTION
// ═════════════════════════════════════════════════════════════
SettingsSectionTitle(title = "Data & Storage", isDarkTheme = isDarkTheme)
Surface(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
SettingsNavigationItem(
icon = Icons.Outlined.Storage,
title = "Storage Usage",
subtitle = "Clear cache, manage data",
onClick = onDataStorageClick,
isDarkTheme = isDarkTheme,
showDivider = true
)
SettingsNavigationItem(
icon = Icons.Outlined.DataUsage,
title = "Network Usage",
subtitle = "Mobile and Wi-Fi",
onClick = onDataStorageClick,
isDarkTheme = isDarkTheme,
showDivider = true
)
SettingsNavigationItem(
icon = Icons.Outlined.DownloadForOffline,
title = "Auto-Download",
subtitle = "Photos, videos, files",
onClick = onDataStorageClick,
isDarkTheme = isDarkTheme
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// 💬 CHAT SETTINGS SECTION
// ═════════════════════════════════════════════════════════════
SettingsSectionTitle(title = "Chat Settings", isDarkTheme = isDarkTheme)
Surface(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
SettingsNavigationItem(
icon = Icons.Outlined.ChatBubbleOutline,
title = "Chat Background",
subtitle = "Set wallpaper for chats",
onClick = onChatSettingsClick,
isDarkTheme = isDarkTheme,
showDivider = true
)
SettingsNavigationItem(
icon = Icons.Outlined.TextFields,
title = "Message Text Size",
subtitle = "Adjust text size in chats",
onClick = onChatSettingsClick,
isDarkTheme = isDarkTheme,
showDivider = true
)
SettingsNavigationItem(
icon = Icons.Outlined.EmojiEmotions,
title = "Stickers & Emojis",
subtitle = "Manage sticker packs",
onClick = onChatSettingsClick,
isDarkTheme = isDarkTheme
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// 🌐 GENERAL SECTION
// ═════════════════════════════════════════════════════════════
SettingsSectionTitle(title = "General", isDarkTheme = isDarkTheme)
Surface(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
SettingsNavigationItem(
icon = Icons.Outlined.Language,
title = "Language",
subtitle = "English",
onClick = onLanguageClick,
isDarkTheme = isDarkTheme
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// SUPPORT SECTION
// ═════════════════════════════════════════════════════════════
SettingsSectionTitle(title = "Support", isDarkTheme = isDarkTheme)
Surface(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
SettingsNavigationItem(
icon = Icons.Outlined.HelpOutline,
title = "Help Center",
subtitle = "FAQ and support",
onClick = onHelpClick,
isDarkTheme = isDarkTheme,
showDivider = true
)
SettingsNavigationItem(
icon = Icons.Outlined.Info,
title = "About",
subtitle = "Version 1.0.0",
onClick = onAboutClick,
isDarkTheme = isDarkTheme
)
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
}
// ═════════════════════════════════════════════════════════════
// 📦 HELPER COMPONENTS
// ═════════════════════════════════════════════════════════════
@Composable
private fun SettingsSectionTitle(title: String, isDarkTheme: Boolean) {
val textColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
Text(
text = title.uppercase(),
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp)
)
}
@Composable
private fun SettingsNavigationItem(
icon: ImageVector,
title: String,
subtitle: String,
onClick: () -> Unit,
isDarkTheme: Boolean,
showDivider: Boolean = false
) {
val textColor = if (isDarkTheme) Color.White else Color(0xFF1A1A1A)
val secondaryTextColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666)
val iconTintColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666)
val dividerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8)
Column {
Row(
modifier =
Modifier.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon
Icon(
imageVector = icon,
contentDescription = null,
tint = iconTintColor,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
// Title and Subtitle
Column(modifier = Modifier.weight(1f)) {
Text(text = title, fontSize = 16.sp, color = textColor)
Spacer(modifier = Modifier.height(2.dp))
Text(text = subtitle, fontSize = 13.sp, color = secondaryTextColor)
}
// Arrow
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
tint = iconTintColor.copy(alpha = 0.5f),
modifier = Modifier.size(20.dp)
)
}
if (showDivider) {
Divider(
color = dividerColor,
thickness = 0.5.dp,
modifier = Modifier.padding(start = 56.dp)
)
}
}
}
@Composable
private fun SettingsToggleItem(
icon: ImageVector,
title: String,
subtitle: String,
isChecked: Boolean,
onToggle: () -> Unit,
isDarkTheme: Boolean
) {
val textColor = if (isDarkTheme) Color.White else Color(0xFF1A1A1A)
val secondaryTextColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666)
val iconTintColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666)
Row(
modifier =
Modifier.fillMaxWidth()
.clickable(onClick = onToggle)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon
Icon(
imageVector = icon,
contentDescription = null,
tint = iconTintColor,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
// Title and Subtitle
Column(modifier = Modifier.weight(1f)) {
Text(text = title, fontSize = 16.sp, color = textColor)
Spacer(modifier = Modifier.height(2.dp))
Text(text = subtitle, fontSize = 13.sp, color = secondaryTextColor)
}
// Toggle Switch
Switch(
checked = isChecked,
onCheckedChange = { onToggle() },
colors =
SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = PrimaryBlue,
uncheckedThumbColor = Color.White,
uncheckedTrackColor =
if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFCCCCCC)
)
)
}
}
private fun getInitials(name: String): String {
if (name.isBlank()) return "?"
val parts = name.trim().split(" ").filter { it.isNotEmpty() }
return when {
parts.isEmpty() -> "?"
parts.size == 1 -> parts[0].take(2).uppercase()
else -> (parts[0].first().toString() + parts[1].first().toString()).uppercase()
}
}