feat: Enhance user profile and settings screens

- Updated ChatDetailScreen to pass user data on profile click.
- Added BackupScreen for seed phrase recovery with password verification.
- Introduced OtherProfileScreen for viewing and managing other users' profiles.
- Created SafetyScreen for account security options including backup and deletion.
- Developed ThemeScreen for theme customization with light, dark, and system modes.
- Implemented UpdatesScreen to display app version and check for updates.
- Removed unused navigation item for updates from ProfileScreen.
This commit is contained in:
2026-01-20 18:59:44 +05:00
parent d78000aa3f
commit c42dd57e3d
8 changed files with 1533 additions and 130 deletions

View File

@@ -0,0 +1,254 @@
package com.rosetta.messenger.ui.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun BackupScreen(
isDarkTheme: Boolean,
onBack: () -> Unit,
onVerifyPassword: (String) -> String? // Returns seed phrase if password is correct
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
var password by remember { mutableStateOf("") }
var seedPhrase by remember { mutableStateOf<String?>(null) }
var passwordVisible by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
// Top Bar
Surface(
modifier = Modifier.fillMaxWidth(),
color = backgroundColor
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = if (isDarkTheme) Color.White else Color.Black
)
}
Text(
text = "Backup",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(start = 8.dp)
)
}
}
// Content
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
// Warning Card
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFFEF4444).copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
tint = Color(0xFFEF4444),
modifier = Modifier
.size(24.dp)
.padding(top = 2.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "If you want to give your recovery phrase to someone, please stop! This may lead to the compromise of your conversations.",
fontSize = 13.sp,
color = textColor,
lineHeight = 18.sp
)
}
}
Spacer(modifier = Modifier.height(24.dp))
if (seedPhrase == null) {
// Password Input
Text(
text = "Confirm Password",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
BasicTextField(
value = password,
onValueChange = { newValue ->
password = newValue
// Try to verify password
val result = onVerifyPassword(newValue)
if (result != null) {
seedPhrase = result
}
},
modifier = Modifier.weight(1f),
textStyle = TextStyle(
color = textColor,
fontSize = 16.sp
),
visualTransformation = if (passwordVisible)
VisualTransformation.None
else
PasswordVisualTransformation(),
decorationBox = { innerTextField ->
if (password.isEmpty()) {
Text(
text = "Enter your password",
color = secondaryTextColor,
fontSize = 16.sp
)
}
innerTextField()
}
)
IconButton(
onClick = { passwordVisible = !passwordVisible }
) {
Icon(
imageVector = if (passwordVisible)
Icons.Filled.Visibility
else
Icons.Filled.VisibilityOff,
contentDescription = if (passwordVisible) "Hide password" else "Show password",
tint = secondaryTextColor
)
}
}
}
Text(
text = "To view your recovery phrase, enter the password you specified when creating your account.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
} else {
// Seed Phrase Display
Text(
text = "Your Recovery Phrase",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFF2E7D32).copy(alpha = 0.1f),
shape = RoundedCornerShape(12.dp)
) {
Column(modifier = Modifier.padding(20.dp)) {
val words = seedPhrase!!.split(" ")
words.chunked(3).forEach { rowWords ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
rowWords.forEachIndexed { index, word ->
Text(
text = "${words.indexOf(word) + 1}. $word",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = textColor,
modifier = Modifier.padding(vertical = 4.dp)
)
}
}
}
}
}
Text(
text = "Please don't share your seed phrase! The administration will never ask you for it. Write it down and store it in a safe place.",
fontSize = 12.sp,
color = Color(0xFFEF4444),
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 12.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
// TODO: Copy to clipboard
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF007AFF)
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Copy to Clipboard",
fontSize = 16.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}

View File

@@ -0,0 +1,205 @@
package com.rosetta.messenger.ui.settings
import androidx.compose.foundation.background
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.outlined.Block
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun OtherProfileScreen(
isDarkTheme: Boolean,
userName: String,
userUsername: String,
userPublicKey: String,
isBlocked: Boolean,
onBack: () -> Unit,
onBlockUser: () -> Unit,
onUnblockUser: () -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.verticalScroll(rememberScrollState())
) {
// Profile Card with avatar
ProfileCard(
name = userName,
username = userUsername,
publicKey = userPublicKey,
isDarkTheme = isDarkTheme,
onBack = onBack,
hasChanges = false,
onSave = {}
)
Spacer(modifier = Modifier.height(16.dp))
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
// Username Section (if available)
if (userUsername.isNotBlank()) {
Text(
text = "Username",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "@$userUsername",
fontSize = 16.sp,
color = textColor,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = { /* TODO: Copy to clipboard */ }
) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
contentDescription = "Copy",
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
}
}
Text(
text = "Username for search user or send message.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(16.dp))
}
// Public Key Section
Text(
text = "Public Key",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = userPublicKey.take(20) + "..." + userPublicKey.takeLast(20),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = { /* TODO: Copy to clipboard */ }
) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
contentDescription = "Copy",
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
}
}
Text(
text = "This is user public key. If user haven't set a @username yet, you can send message using public key.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Block/Unblock Section
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
if (isBlocked) {
ProfileNavigationItem(
icon = Icons.Outlined.Block,
iconBackground = Color(0xFF2E7D32), // green
title = "Unblock User",
subtitle = "Allow this user to message you",
onClick = onUnblockUser,
isDarkTheme = isDarkTheme,
hideChevron = true,
textColor = Color(0xFF2E7D32)
)
} else {
ProfileNavigationItem(
icon = Icons.Outlined.Block,
iconBackground = Color(0xFFEF4444), // red
title = "Block this user",
subtitle = "Prevent this user from messaging you",
onClick = onBlockUser,
isDarkTheme = isDarkTheme,
hideChevron = true,
textColor = Color(0xFFEF4444)
)
}
}
}
Text(
text = if (isBlocked) {
"If you want the user to be able to send you messages again, you can unblock them. You can block them later."
} else {
"The person will no longer be able to message you if you block them. You can unblock them later."
},
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(32.dp))
}
}
}

View File

@@ -101,8 +101,7 @@ fun ProfileScreen(
onSaveProfile: (name: String, username: String) -> Unit,
onLogout: () -> Unit,
onNavigateToTheme: () -> Unit = {},
onNavigateToSafety: () -> Unit = {},
onNavigateToUpdates: () -> Unit = {}
onNavigateToSafety: () -> Unit = {}
) {
// Цвета в зависимости от темы - такие же как в ChatsListScreen
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
@@ -210,15 +209,6 @@ fun ProfileScreen(
shape = RoundedCornerShape(16.dp)
) {
Column {
ProfileNavigationItem(
icon = Icons.Outlined.Refresh,
iconBackground = Color(0xFF84CC16), // lime-600
title = "Updates",
subtitle = "Check for new versions",
onClick = onNavigateToUpdates,
isDarkTheme = isDarkTheme,
showDivider = true
)
ProfileNavigationItem(
icon = Icons.Outlined.Brush,
iconBackground = Color(0xFF6366F1), // indigo
@@ -558,7 +548,7 @@ private fun ProfileCopyField(
}
@Composable
private fun ProfileNavigationItem(
fun ProfileNavigationItem(
icon: ImageVector,
iconBackground: Color,
title: String,

View File

@@ -0,0 +1,228 @@
package com.rosetta.messenger.ui.settings
import androidx.compose.foundation.background
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.Warning
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun SafetyScreen(
isDarkTheme: Boolean,
accountPublicKey: String,
onBack: () -> Unit,
onBackupClick: () -> Unit,
onDeleteAccount: () -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
// Top Bar
Surface(
modifier = Modifier.fillMaxWidth(),
color = backgroundColor
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = if (isDarkTheme) Color.White else Color.Black
)
}
Text(
text = "Safety & Security",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(start = 8.dp)
)
}
}
// Content
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
// Warning Card
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFFFF9800).copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
tint = Color(0xFFFF9800),
modifier = Modifier
.size(24.dp)
.padding(top = 2.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Never share your private key or seed phrase with anyone. Keep your backup secure and private.",
fontSize = 13.sp,
color = textColor,
lineHeight = 18.sp
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Public Key Section
Text(
text = "Public Key",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = accountPublicKey.take(20) + "..." + accountPublicKey.takeLast(20),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = { /* TODO: Copy to clipboard */ }
) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
contentDescription = "Copy",
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
}
}
Text(
text = "This is your public key. You can safely share this with others so they can message you.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Backup Section
Text(
text = "Backup & Recovery",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
ProfileNavigationItem(
icon = Icons.Outlined.ContentCopy,
iconBackground = Color(0xFFFF9800),
title = "Backup Seed Phrase",
subtitle = "View and save your recovery phrase",
onClick = onBackupClick,
isDarkTheme = isDarkTheme,
textColor = Color(0xFFFF9800)
)
}
}
Text(
text = "Please save your seed phrase, it is necessary for future access to your conversations. Do not share this seed phrase with anyone.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Danger Zone
Text(
text = "Danger Zone",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFFEF4444),
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
ProfileNavigationItem(
icon = Icons.Outlined.ContentCopy,
iconBackground = Color(0xFFEF4444),
title = "Delete Account",
subtitle = "Permanently delete your account",
onClick = onDeleteAccount,
isDarkTheme = isDarkTheme,
hideChevron = true,
textColor = Color(0xFFEF4444)
)
}
}
Text(
text = "This action cannot be undone. It will result in the complete deletion of account data from your device and server.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
}
}
}

View File

@@ -0,0 +1,501 @@
package com.rosetta.messenger.ui.settings
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
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.Check
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.Smartphone
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
@Composable
fun ThemeScreen(
isDarkTheme: Boolean,
onBack: () -> Unit,
onThemeChange: (Boolean) -> Unit
) {
val backgroundColor by animateColorAsState(
targetValue = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF),
animationSpec = tween(300),
label = "backgroundColor"
)
val surfaceColor by animateColorAsState(
targetValue = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7),
animationSpec = tween(300),
label = "surfaceColor"
)
val textColor by animateColorAsState(
targetValue = if (isDarkTheme) Color.White else Color.Black,
animationSpec = tween(300),
label = "textColor"
)
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
// Theme mode: "light", "dark", "auto"
var themeMode by remember { mutableStateOf(if (isDarkTheme) "dark" else "light") }
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
// Top Bar
Surface(
modifier = Modifier.fillMaxWidth(),
color = backgroundColor
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = textColor
)
}
Text(
text = "Theme",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(start = 8.dp)
)
}
}
// Content
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
// ═══════════════════════════════════════════════════════
// 🎨 THEME PREVIEW CARDS - Like Desktop Version
// ═══════════════════════════════════════════════════════
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Theme",
fontSize = 14.sp,
color = secondaryTextColor,
modifier = Modifier.padding(bottom = 12.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// Light Theme Preview
ThemePreviewCard(
isSelected = themeMode == "light",
isDark = false,
label = "Light",
onClick = {
themeMode = "light"
onThemeChange(false)
}
)
Spacer(modifier = Modifier.width(12.dp))
// Dark Theme Preview
ThemePreviewCard(
isSelected = themeMode == "dark",
isDark = true,
label = "Dark",
onClick = {
themeMode = "dark"
onThemeChange(true)
}
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// ═══════════════════════════════════════════════════════
// 🔘 THEME MODE SELECTOR
// ═══════════════════════════════════════════════════════
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
ThemeModeOption(
icon = Icons.Filled.LightMode,
title = "Light",
subtitle = "Always use light theme",
isSelected = themeMode == "light",
onClick = {
themeMode = "light"
onThemeChange(false)
},
textColor = textColor,
secondaryTextColor = secondaryTextColor,
showDivider = true,
isDarkTheme = isDarkTheme
)
ThemeModeOption(
icon = Icons.Filled.DarkMode,
title = "Dark",
subtitle = "Always use dark theme",
isSelected = themeMode == "dark",
onClick = {
themeMode = "dark"
onThemeChange(true)
},
textColor = textColor,
secondaryTextColor = secondaryTextColor,
showDivider = true,
isDarkTheme = isDarkTheme
)
ThemeModeOption(
icon = Icons.Filled.Smartphone,
title = "System",
subtitle = "Match system settings",
isSelected = themeMode == "auto",
onClick = {
themeMode = "auto"
// For now, we'll just keep the current theme
},
textColor = textColor,
secondaryTextColor = secondaryTextColor,
showDivider = false,
isDarkTheme = isDarkTheme
)
}
}
Text(
text = "If you choose the automatic mode, the theme will change depending on your system settings. We recommend using automatic mode.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 12.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(24.dp))
// ═══════════════════════════════════════════════════════
// 💬 MESSAGE PREVIEW
// ═══════════════════════════════════════════════════════
Text(
text = "Preview",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 12.dp, start = 4.dp)
)
Surface(
modifier = Modifier
.fillMaxWidth()
.height(220.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
// Received message
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
MessageBubble(
text = "Hey! How are you? 👋",
isFromMe = false,
isDarkTheme = isDarkTheme
)
}
Spacer(modifier = Modifier.height(8.dp))
// Sent message
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
MessageBubble(
text = "I'm great, thanks! 🎉",
isFromMe = true,
isDarkTheme = isDarkTheme
)
}
Spacer(modifier = Modifier.height(8.dp))
// Another received message
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
MessageBubble(
text = "Love the new theme! 🌙",
isFromMe = false,
isDarkTheme = isDarkTheme
)
}
}
}
}
}
}
}
@Composable
private fun ThemePreviewCard(
isSelected: Boolean,
isDark: Boolean,
label: String,
onClick: () -> Unit
) {
val cardBg = if (isDark) Color(0xFF1C1C1E) else Color(0xFFF5F5F5)
val messageBg = if (isDark) Color(0xFF3A3A3C) else Color(0xFFE5E5EA)
val myMessageBg = Color(0xFF007AFF)
val borderColor = if (isSelected) Color(0xFF007AFF) else Color.Transparent
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.clickable(onClick = onClick)
) {
// Preview Card
Box(
modifier = Modifier
.size(width = 130.dp, height = 90.dp)
.clip(RoundedCornerShape(12.dp))
.border(
width = if (isSelected) 2.dp else 1.dp,
color = if (isSelected) borderColor else Color(0xFF3A3A3C).copy(alpha = 0.3f),
shape = RoundedCornerShape(12.dp)
)
.background(cardBg)
.padding(8.dp)
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly
) {
// Fake message bubbles
Box(
modifier = Modifier
.fillMaxWidth(0.7f)
.height(14.dp)
.clip(RoundedCornerShape(7.dp))
.background(messageBg)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Box(
modifier = Modifier
.fillMaxWidth(0.6f)
.height(14.dp)
.clip(RoundedCornerShape(7.dp))
.background(myMessageBg)
)
}
Box(
modifier = Modifier
.fillMaxWidth(0.5f)
.height(14.dp)
.clip(RoundedCornerShape(7.dp))
.background(messageBg)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
// Label with checkmark
Row(
verticalAlignment = Alignment.CenterVertically
) {
if (isSelected) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
tint = Color(0xFF007AFF),
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
text = label,
fontSize = 13.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
color = if (isSelected) Color(0xFF007AFF) else Color(0xFF8E8E93)
)
}
}
}
@Composable
private fun ThemeModeOption(
icon: ImageVector,
title: String,
subtitle: String,
isSelected: Boolean,
onClick: () -> Unit,
textColor: Color,
secondaryTextColor: Color,
showDivider: Boolean,
isDarkTheme: Boolean
) {
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
Column(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon with background
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(8.dp))
.background(
when (title) {
"Light" -> Color(0xFFFFA500)
"Dark" -> Color(0xFF6366F1)
else -> Color(0xFF8E8E93)
}
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = textColor
)
Text(
text = subtitle,
fontSize = 12.sp,
color = secondaryTextColor
)
}
// Radio button
Box(
modifier = Modifier
.size(24.dp)
.border(
width = 2.dp,
color = if (isSelected) Color(0xFF007AFF) else secondaryTextColor.copy(alpha = 0.5f),
shape = CircleShape
)
.padding(4.dp)
.background(
color = if (isSelected) Color(0xFF007AFF) else Color.Transparent,
shape = CircleShape
)
)
}
if (showDivider) {
Divider(
color = dividerColor,
thickness = 0.5.dp,
modifier = Modifier.padding(start = 64.dp)
)
}
}
}
@Composable
private fun MessageBubble(
text: String,
isFromMe: Boolean,
isDarkTheme: Boolean
) {
val bubbleColor = if (isFromMe) {
Color(0xFF007AFF)
} else {
if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE5E5EA)
}
val textColorMsg = if (isFromMe) {
Color.White
} else {
if (isDarkTheme) Color.White else Color.Black
}
Surface(
color = bubbleColor,
shape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = if (isFromMe) 16.dp else 4.dp,
bottomEnd = if (isFromMe) 4.dp else 16.dp
)
) {
Text(
text = text,
color = textColorMsg,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
)
}
}

View File

@@ -0,0 +1,170 @@
package com.rosetta.messenger.ui.settings
import androidx.compose.foundation.background
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.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun UpdatesScreen(
isDarkTheme: Boolean,
onBack: () -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
// Top Bar
Surface(
modifier = Modifier.fillMaxWidth(),
color = backgroundColor
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = if (isDarkTheme) Color.White else Color.Black
)
}
Text(
text = "Updates",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(start = 8.dp)
)
}
}
// Content
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
// Info Card
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFF2E7D32).copy(alpha = 0.2f),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "✓ App is up to date",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF2E7D32)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "You're using the latest version",
fontSize = 12.sp,
color = secondaryTextColor
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Version Info
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Application Version",
fontSize = 14.sp,
color = textColor
)
Text(
text = "1.0.0",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = secondaryTextColor
)
}
Spacer(modifier = Modifier.height(12.dp))
Divider(color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8))
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Build Number",
fontSize = 14.sp,
color = textColor
)
Text(
text = "100",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = secondaryTextColor
)
}
}
}
Text(
text = "We recommend always keeping the app up to date to improve visual effects and have the latest features.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(16.dp))
// Check for updates button
Button(
onClick = { /* TODO: Implement update check */ },
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF007AFF)
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Check for Updates",
fontSize = 16.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}