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:
k1ngsterr1
2026-01-20 18:59:44 +05:00
parent d78000aa3f
commit c42dd57e3d
8 changed files with 1533 additions and 130 deletions

View File

@@ -39,7 +39,12 @@ 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.BackupScreen
import com.rosetta.messenger.ui.settings.OtherProfileScreen
import com.rosetta.messenger.ui.settings.ProfileScreen
import com.rosetta.messenger.ui.settings.SafetyScreen
import com.rosetta.messenger.ui.settings.ThemeScreen
import com.rosetta.messenger.ui.settings.UpdatesScreen
import com.rosetta.messenger.ui.splash.SplashScreen
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
import java.text.SimpleDateFormat
@@ -439,79 +444,152 @@ fun MainScreen(
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
var showSearchScreen by remember { mutableStateOf(false) }
var showProfileScreen by remember { mutableStateOf(false) }
var showOtherProfileScreen by remember { mutableStateOf(false) }
var selectedOtherUser by remember { mutableStateOf<SearchUser?>(null) }
// 🔥 TELEGRAM-STYLE анимация - чистый slide БЕЗ прозрачности
AnimatedContent(
targetState = Pair(Pair(selectedUser, showSearchScreen), showProfileScreen),
transitionSpec = {
val isEnteringChat = targetState.first.first != null && initialState.first.first == null
val isExitingChat = targetState.first.first == null && initialState.first.first != null
val isEnteringSearch = targetState.first.second && !initialState.first.second
val isExitingSearch = !targetState.first.second && initialState.first.second
val isEnteringProfile = targetState.second && !initialState.second
val isExitingProfile = !targetState.second && initialState.second
// Дополнительные экраны настроек
var showUpdatesScreen by remember { mutableStateOf(false) }
var showThemeScreen by remember { mutableStateOf(false) }
var showSafetyScreen by remember { mutableStateOf(false) }
var showBackupScreen by remember { mutableStateOf(false) }
when {
// 🚀 Вход в чат - плавный fade
isEnteringChat -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
// 🔥 Простая навигация с fade-in анимацией
Box(modifier = Modifier.fillMaxSize()) {
// Base layer - chats list
androidx.compose.animation.AnimatedVisibility(
visible = !showBackupScreen && !showSafetyScreen && !showThemeScreen &&
!showUpdatesScreen && selectedUser == null && !showSearchScreen &&
!showProfileScreen && !showOtherProfileScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
ChatsListScreen(
isDarkTheme = isDarkTheme,
accountName = accountName,
accountPhone = accountPhone,
accountPublicKey = accountPublicKey,
accountPrivateKey = accountPrivateKey,
privateKeyHash = privateKeyHash,
onToggleTheme = onToggleTheme,
onProfileClick = {
showProfileScreen = true
},
onNewGroupClick = {
// TODO: Navigate to new group
},
onCallsClick = {
// TODO: Navigate to calls
},
onSavedMessagesClick = {
// Открываем чат с самим собой (Saved Messages)
selectedUser =
SearchUser(
title = "Saved Messages",
username = "",
publicKey = accountPublicKey,
verified = 0,
online = 1
)
},
onSettingsClick = { showProfileScreen = true },
onInviteFriendsClick = {
// TODO: Share invite link
},
onSearchClick = { showSearchScreen = true },
onNewChat = {
// TODO: Show new chat screen
},
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
onLogout = onLogout
)
}
// Other screens with fade animation
androidx.compose.animation.AnimatedVisibility(
visible = showBackupScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (showBackupScreen) {
BackupScreen(
isDarkTheme = isDarkTheme,
onBack = { showBackupScreen = false },
onVerifyPassword = { password ->
// TODO: Implement password verification
if (password == "test") {
"word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12"
} else null
}
)
}
}
// 🔙 Выход из чата - плавный fade
isExitingChat -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
androidx.compose.animation.AnimatedVisibility(
visible = showSafetyScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (showSafetyScreen) {
SafetyScreen(
isDarkTheme = isDarkTheme,
accountPublicKey = accountPublicKey,
onBack = { showSafetyScreen = false },
onBackupClick = { showBackupScreen = true },
onDeleteAccount = {
// TODO: Implement account deletion
Log.d("MainActivity", "Delete account requested")
}
)
}
}
// 🔍 Вход в Search - плавный fade
isEnteringSearch -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
androidx.compose.animation.AnimatedVisibility(
visible = showThemeScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (showThemeScreen) {
ThemeScreen(
isDarkTheme = isDarkTheme,
onBack = { showThemeScreen = false },
onThemeChange = { isDark ->
onToggleTheme()
}
)
}
}
// 🔙 Выход из Search - плавный fade
isEnteringSearch -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
}
androidx.compose.animation.AnimatedVisibility(
visible = showUpdatesScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (showUpdatesScreen) {
UpdatesScreen(
isDarkTheme = isDarkTheme,
onBack = { showUpdatesScreen = false }
)
}
}
// 🔙 Выход из Search - плавный fade
isExitingSearch -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
}
// 👤 Вход в Profile - плавный fade
isEnteringProfile -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
}
// 🔙 Выход из Profile - плавный fade
isExitingProfile -> {
fadeIn(animationSpec = tween(200)) togetherWith
fadeOut(animationSpec = tween(150))
}
// Default - мгновенный переход
else -> {
EnterTransition.None togetherWith ExitTransition.None
}
}
},
label = "screenNavigation"
) { (chatAndSearchState, isProfileOpen) ->
val (currentUser, isSearchOpen) = chatAndSearchState
when {
currentUser != null -> {
androidx.compose.animation.AnimatedVisibility(
visible = selectedUser != null,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (selectedUser != null) {
// Экран чата
ChatDetailScreen(
user = currentUser,
user = selectedUser!!,
currentUserPublicKey = accountPublicKey,
currentUserPrivateKey = accountPrivateKey,
isDarkTheme = isDarkTheme,
onBack = { selectedUser = null },
onUserProfileClick = { user ->
// Открываем профиль другого пользователя
selectedOtherUser = user
showOtherProfileScreen = true
},
onNavigateToChat = { publicKey ->
// 📨 Forward: переход в выбранный чат
// Нужно получить SearchUser из публичного ключа
@@ -528,7 +606,14 @@ fun MainScreen(
}
)
}
isSearchOpen -> {
}
androidx.compose.animation.AnimatedVisibility(
visible = showSearchScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (showSearchScreen) {
// Экран поиска
SearchScreen(
privateKeyHash = privateKeyHash,
@@ -542,7 +627,14 @@ fun MainScreen(
}
)
}
isProfileOpen -> {
}
androidx.compose.animation.AnimatedVisibility(
visible = showProfileScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (showProfileScreen) {
// Экран профиля
ProfileScreen(
isDarkTheme = isDarkTheme,
@@ -554,67 +646,30 @@ fun MainScreen(
// TODO: Save profile changes
Log.d("MainActivity", "Saving profile: name=$name, username=$username")
},
onLogout = {
// TODO: Implement logout
Log.d("MainActivity", "Logout requested")
},
onLogout = onLogout,
onNavigateToTheme = {
// Toggle theme for now
onToggleTheme()
showThemeScreen = true
},
onNavigateToSafety = {
// TODO: Navigate to safety screen
Log.d("MainActivity", "Navigate to safety")
},
onNavigateToUpdates = {
// TODO: Navigate to updates screen
Log.d("MainActivity", "Navigate to updates")
showSafetyScreen = true
}
)
}
else -> {
// Список чатов
ChatsListScreen(
isDarkTheme = isDarkTheme,
accountName = accountName,
accountPhone = accountPhone,
accountPublicKey = accountPublicKey,
accountPrivateKey = accountPrivateKey,
privateKeyHash = privateKeyHash,
onToggleTheme = onToggleTheme,
onProfileClick = {
showProfileScreen = true
},
onNewGroupClick = {
// TODO: Navigate to new group
},
onContactsClick = {
// TODO: Navigate to contacts
},
onCallsClick = {
// TODO: Navigate to calls
},
onSavedMessagesClick = {
// Открываем чат с самим собой (Saved Messages)
selectedUser =
SearchUser(
title = "Saved Messages",
username = "",
publicKey = accountPublicKey,
verified = 0,
online = 1
)
},
onSettingsClick = { showProfileScreen = true },
onInviteFriendsClick = {
// TODO: Share invite link
},
onSearchClick = { showSearchScreen = true },
onNewChat = {
// TODO: Show new chat screen
},
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
onLogout = onLogout
}
androidx.compose.animation.AnimatedVisibility(
visible = showOtherProfileScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (showOtherProfileScreen && selectedOtherUser != null) {
OtherProfileScreen(
user = selectedOtherUser!!,
isDarkTheme = isDarkTheme,
onBack = {
showOtherProfileScreen = false
selectedOtherUser = null
}
)
}
}

View File

@@ -237,7 +237,7 @@ fun ChatDetailScreen(
currentUserPrivateKey: String,
isDarkTheme: Boolean,
onBack: () -> Unit,
onUserProfileClick: () -> Unit = {},
onUserProfileClick: (SearchUser) -> Unit = {},
onNavigateToChat: (String) -> Unit = {}, // 📨 Callback для навигации в другой чат (Forward)
viewModel: ChatViewModel = viewModel()
) {
@@ -964,7 +964,7 @@ fun ChatDetailScreen(
?.hide()
focusManager
.clearFocus()
onUserProfileClick()
onUserProfileClick(user)
},
contentAlignment =
Alignment.Center
@@ -1032,7 +1032,7 @@ fun ChatDetailScreen(
?.hide()
focusManager
.clearFocus()
onUserProfileClick()
onUserProfileClick(user)
}
) {
Row(

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
)
}
}
}
}