android: redesign settings and profile screens to telegram-like layout
This commit is contained in:
@@ -719,3 +719,15 @@
|
||||
- Removed fallback read-pointer advancement in `ChatViewModel.acknowledgeLatestMessages(...)` that previously moved `lastReadMessageId` by latest loaded message id.
|
||||
- Read pointer is now advanced only via `onVisibleIncomingMessageId(...)` from visible incoming rows in `ChatScreen`.
|
||||
- This prevents read acknowledgements from overshooting beyond what user actually saw during refresh/recompose scenarios.
|
||||
|
||||
### Step 109 - Telegram-like Settings/Profile visual refresh
|
||||
- Redesigned `SettingsScreen` to Telegram-inspired dark card layout:
|
||||
- profile header card with avatar/name/email/username,
|
||||
- grouped settings rows with material icons,
|
||||
- appearance controls (Light/Dark/System),
|
||||
- quick security/help sections and preserved logout/back actions.
|
||||
- Redesigned `ProfileScreen` to Telegram-inspired structure:
|
||||
- gradient hero header with centered avatar, status, and action buttons,
|
||||
- primary profile info card,
|
||||
- tab-like section (`Posts/Archived/Gifts`) with placeholder content,
|
||||
- inline edit card (name/username/bio/avatar URL) with existing save/upload behavior preserved.
|
||||
|
||||
@@ -8,16 +8,17 @@ import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -25,34 +26,49 @@ import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
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.AddAPhoto
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import coil.compose.AsyncImage
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
private val ProfileBg = Color(0xFF0D0F12)
|
||||
private val ProfileCard = Color(0xFF171A1F)
|
||||
private val ProfileMuted = Color(0xFFA2A7B3)
|
||||
|
||||
@Composable
|
||||
fun ProfileRoute(
|
||||
onBackToChats: () -> Unit,
|
||||
@@ -69,7 +85,6 @@ fun ProfileRoute(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun ProfileScreen(
|
||||
onBackToChats: () -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
@@ -82,6 +97,8 @@ fun ProfileScreen(
|
||||
var username by remember(profile?.username) { mutableStateOf(profile?.username.orEmpty()) }
|
||||
var bio by remember(profile?.bio) { mutableStateOf(profile?.bio.orEmpty()) }
|
||||
var avatarUrl by remember(profile?.avatarUrl) { mutableStateOf(profile?.avatarUrl.orEmpty()) }
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
var editMode by remember { mutableStateOf(false) }
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -101,7 +118,7 @@ fun ProfileScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val context = LocalContext.current
|
||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||
val pickAvatarLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent(),
|
||||
@@ -119,127 +136,296 @@ fun ProfileScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.navigationBarsPadding(),
|
||||
.background(ProfileBg)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isTabletLayout) Modifier.widthIn(max = 720.dp) else Modifier)
|
||||
.padding(bottom = 96.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { Text("Profile") },
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
.padding(bottom = 96.dp),
|
||||
) {
|
||||
if (!avatarUrl.isBlank()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = avatarUrl,
|
||||
contentDescription = "Avatar",
|
||||
modifier = Modifier
|
||||
.size(180.dp)
|
||||
.aspectRatio(1f)
|
||||
.clip(CircleShape)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
shape = CircleShape,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(305.dp)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color(0xFF9F4649), Color(0xFFB85C61), Color(0xFF9D4A4C)),
|
||||
),
|
||||
)
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
if (avatarUrl.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = avatarUrl,
|
||||
contentDescription = "Avatar",
|
||||
modifier = Modifier
|
||||
.size(108.dp)
|
||||
.clip(CircleShape)
|
||||
.border(1.dp, Color(0x88FFFFFF), CircleShape),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(108.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0x55FFFFFF)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = name.firstOrNull()?.uppercase() ?: "?",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = if (name.isBlank()) "User" else name,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text("online", color = Color(0xE6FFFFFF), style = MaterialTheme.typography.bodyLarge)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
HeroActionButton(
|
||||
label = "Choose photo",
|
||||
icon = Icons.Filled.AddAPhoto,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
pickAvatarLauncher.launch("image/*")
|
||||
}
|
||||
HeroActionButton(
|
||||
label = if (editMode) "Editing" else "Edit",
|
||||
icon = Icons.Filled.Edit,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
editMode = !editMode
|
||||
}
|
||||
HeroActionButton(
|
||||
label = "Settings",
|
||||
icon = Icons.Filled.Settings,
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
onOpenSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Surface(
|
||||
color = ProfileCard,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = (-22).dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
ProfileInfoRow("Email", profile?.email.orEmpty())
|
||||
ProfileInfoRow("Bio", bio.ifBlank { "Not set" })
|
||||
ProfileInfoRow("Username", if (username.isBlank()) "Not set" else "@$username")
|
||||
ProfileInfoRow("Name", name.ifBlank { "Not set" })
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = ProfileCard,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ProfileTab("Posts", selectedTab == 0, modifier = Modifier.weight(1f)) { selectedTab = 0 }
|
||||
ProfileTab("Archived", selectedTab == 1, modifier = Modifier.weight(1f)) { selectedTab = 1 }
|
||||
ProfileTab("Gifts", selectedTab == 2, modifier = Modifier.weight(1f)) { selectedTab = 2 }
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = ProfileCard,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(18.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text("No posts yet", color = Color.White, style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
"Publish something in your profile.",
|
||||
color = ProfileMuted,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Button(onClick = {}, modifier = Modifier.padding(top = 6.dp)) {
|
||||
Text("Add")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (editMode) {
|
||||
Surface(
|
||||
color = ProfileCard,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text("Edit profile", color = Color.White, style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Name") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = bio,
|
||||
onValueChange = { bio = it },
|
||||
label = { Text("Bio") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = avatarUrl,
|
||||
onValueChange = { avatarUrl = it },
|
||||
label = { Text("Avatar URL") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.updateProfile(
|
||||
name = name,
|
||||
username = username,
|
||||
bio = bio.ifBlank { null },
|
||||
avatarUrl = avatarUrl.ifBlank { null },
|
||||
)
|
||||
},
|
||||
enabled = !state.isSaving && name.isNotBlank() && username.isNotBlank(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Save profile")
|
||||
}
|
||||
if (state.isSaving) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(22.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.message.isNullOrBlank()) {
|
||||
Text(
|
||||
text = state.message.orEmpty(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
)
|
||||
}
|
||||
if (!state.errorMessage.isNullOrBlank()) {
|
||||
Text(
|
||||
text = state.errorMessage.orEmpty(),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onBackToChats,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 2.dp),
|
||||
) {
|
||||
Text("Back to chats")
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Button(onClick = { pickAvatarLauncher.launch("image/*") }, enabled = !state.isSaving) {
|
||||
Text("Upload avatar")
|
||||
}
|
||||
if (state.isSaving) {
|
||||
CircularProgressIndicator(modifier = Modifier.padding(4.dp))
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Name") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = bio,
|
||||
onValueChange = { bio = it },
|
||||
label = { Text("Bio") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = avatarUrl,
|
||||
onValueChange = { avatarUrl = it },
|
||||
label = { Text("Avatar URL") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeroActionButton(
|
||||
label: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
color = Color(0x40FFFFFF),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.updateProfile(
|
||||
name = name,
|
||||
username = username,
|
||||
bio = bio.ifBlank { null },
|
||||
avatarUrl = avatarUrl.ifBlank { null },
|
||||
)
|
||||
},
|
||||
enabled = !state.isSaving && name.isNotBlank() && username.isNotBlank(),
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Save profile")
|
||||
}
|
||||
if (!state.message.isNullOrBlank()) {
|
||||
Text(
|
||||
text = state.message!!,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
if (!state.errorMessage.isNullOrBlank()) {
|
||||
Text(
|
||||
text = state.errorMessage!!,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onOpenSettings,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Open settings")
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onBackToChats,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Back to chats")
|
||||
}
|
||||
}
|
||||
Icon(icon, contentDescription = null)
|
||||
Text(label, modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileInfoRow(label: String, value: String) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(text = value, style = MaterialTheme.typography.titleLarge, color = Color.White)
|
||||
Text(text = label, style = MaterialTheme.typography.bodyMedium, color = ProfileMuted)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileTab(
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
if (selected) {
|
||||
Button(onClick = onClick, modifier = modifier) { Text(label) }
|
||||
} else {
|
||||
OutlinedButton(onClick = onClick, modifier = modifier) { Text(label) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Uri.toSquareJpeg(context: Context): ByteArray? {
|
||||
val bitmap = runCatching {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
|
||||
@@ -1,45 +1,72 @@
|
||||
package ru.daemonlord.messenger.ui.settings
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
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.automirrored.filled.HelpOutline
|
||||
import androidx.compose.material.icons.filled.BatterySaver
|
||||
import androidx.compose.material.icons.filled.ChatBubbleOutline
|
||||
import androidx.compose.material.icons.filled.DataUsage
|
||||
import androidx.compose.material.icons.filled.Devices
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
||||
|
||||
private val SettingsBackground = Color(0xFF0D0F12)
|
||||
private val SettingsCard = Color(0xFF1A1D23)
|
||||
private val SettingsMuted = Color(0xFF9EA3B0)
|
||||
|
||||
@Composable
|
||||
fun SettingsRoute(
|
||||
onBackToChats: () -> Unit,
|
||||
@@ -58,7 +85,6 @@ fun SettingsRoute(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun SettingsScreen(
|
||||
onBackToChats: () -> Unit,
|
||||
onOpenProfile: () -> Unit,
|
||||
@@ -68,20 +94,12 @@ fun SettingsScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val profile = state.profile
|
||||
var privacyPm by remember(profile?.privacyPrivateMessages) { mutableStateOf(profile?.privacyPrivateMessages ?: "everyone") }
|
||||
var privacyLastSeen by remember(profile?.privacyLastSeen) { mutableStateOf(profile?.privacyLastSeen ?: "everyone") }
|
||||
var privacyAvatar by remember(profile?.privacyAvatar) { mutableStateOf(profile?.privacyAvatar ?: "everyone") }
|
||||
var privacyGroupInvites by remember(profile?.privacyGroupInvites) { mutableStateOf(profile?.privacyGroupInvites ?: "everyone") }
|
||||
var twoFactorCode by remember { mutableStateOf("") }
|
||||
var recoveryRegenerateCode by remember { mutableStateOf("") }
|
||||
var blockUserIdInput by remember { mutableStateOf("") }
|
||||
var nightMode by remember { mutableIntStateOf(AppCompatDelegate.getDefaultNightMode()) }
|
||||
val scrollState = rememberScrollState()
|
||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||
var nightMode by remember { mutableIntStateOf(AppCompatDelegate.getDefaultNightMode()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
viewModel.refreshRecoveryStatus()
|
||||
onMainBarVisibilityChanged(true)
|
||||
}
|
||||
LaunchedEffect(scrollState) {
|
||||
@@ -100,6 +118,7 @@ fun SettingsScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(SettingsBackground)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
@@ -107,246 +126,214 @@ fun SettingsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isTabletLayout) Modifier.widthIn(max = 720.dp) else Modifier)
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.padding(bottom = 96.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text("Appearance", style = MaterialTheme.typography.titleMedium)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_NO
|
||||
},
|
||||
) { Text("Light") }
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_YES
|
||||
},
|
||||
) { Text("Dark") }
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
},
|
||||
) { Text("System") }
|
||||
}
|
||||
Text(
|
||||
text = when (nightMode) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> "Current theme: Dark"
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> "Current theme: Light"
|
||||
else -> "Current theme: System"
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
Text("Sessions", style = MaterialTheme.typography.titleMedium)
|
||||
if (state.sessions.isEmpty()) {
|
||||
Text("No active sessions", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
} else {
|
||||
state.sessions.forEach { session ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(session.userAgent ?: "Unknown device", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(session.ipAddress ?: "Unknown IP", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.revokeSession(session.jti) },
|
||||
enabled = !state.isSaving && session.current != true,
|
||||
) {
|
||||
Text("Revoke")
|
||||
}
|
||||
Text("Settings", style = MaterialTheme.typography.headlineSmall, color = Color.White)
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(22.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = viewModel::revokeAllSessions,
|
||||
enabled = !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Revoke all sessions")
|
||||
}
|
||||
|
||||
Text("Privacy", style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedTextField(
|
||||
value = privacyPm,
|
||||
onValueChange = { privacyPm = it },
|
||||
label = { Text("PM privacy (everyone/contacts/nobody)") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = privacyLastSeen,
|
||||
onValueChange = { privacyLastSeen = it },
|
||||
label = { Text("Last seen privacy") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = privacyAvatar,
|
||||
onValueChange = { privacyAvatar = it },
|
||||
label = { Text("Avatar privacy") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = privacyGroupInvites,
|
||||
onValueChange = { privacyGroupInvites = it },
|
||||
label = { Text("Group invites privacy") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.updatePrivacy(
|
||||
privateMessages = privacyPm,
|
||||
lastSeen = privacyLastSeen,
|
||||
avatar = privacyAvatar,
|
||||
groupInvites = privacyGroupInvites,
|
||||
)
|
||||
},
|
||||
enabled = !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Save privacy")
|
||||
}
|
||||
|
||||
Text("Blocked users", style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedTextField(
|
||||
value = blockUserIdInput,
|
||||
onValueChange = { blockUserIdInput = it.filter { ch -> ch.isDigit() } },
|
||||
label = { Text("User ID to block") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
blockUserIdInput.toLongOrNull()?.let { viewModel.blockUser(it) }
|
||||
blockUserIdInput = ""
|
||||
},
|
||||
enabled = !state.isSaving && blockUserIdInput.isNotBlank(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Block user by ID")
|
||||
}
|
||||
if (state.blockedUsers.isEmpty()) {
|
||||
Text("Blocked list is empty", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
} else {
|
||||
state.blockedUsers.forEach { user ->
|
||||
Surface(
|
||||
color = SettingsCard,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(user.name)
|
||||
if (!user.username.isNullOrBlank()) {
|
||||
Text("@${user.username}", style = MaterialTheme.typography.bodySmall)
|
||||
if (!profile?.avatarUrl.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = profile?.avatarUrl,
|
||||
contentDescription = "Profile avatar",
|
||||
modifier = Modifier
|
||||
.size(58.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(58.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF6650A4)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = profile?.name?.firstOrNull()?.uppercase() ?: "?",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.unblockUser(user.id) }) {
|
||||
Text("Unblock")
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = profile?.name ?: "Loading...",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = listOfNotNull(
|
||||
profile?.email,
|
||||
profile?.username?.takeIf { it.isNotBlank() }?.let { "@$it" },
|
||||
).joinToString(" • "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = SettingsMuted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onOpenProfile) {
|
||||
Icon(Icons.Filled.Person, contentDescription = "Profile", tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("2FA", style = MaterialTheme.typography.titleMedium)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = viewModel::setupTwoFactor, enabled = !state.isSaving) {
|
||||
Text("Setup")
|
||||
SettingsSectionCard {
|
||||
SettingsRow(Icons.Filled.Person, "Account", "Name, username, bio") { onOpenProfile() }
|
||||
SettingsRow(Icons.Filled.ChatBubbleOutline, "Chat settings", "Wallpaper, media, interactions") {}
|
||||
SettingsRow(Icons.Filled.Lock, "Privacy", "Last seen, messages, avatar") {}
|
||||
SettingsRow(Icons.Filled.Notifications, "Notifications", "Sounds and counters") {}
|
||||
SettingsRow(Icons.Filled.DataUsage, "Data and storage", "Media auto-download") {}
|
||||
SettingsRow(Icons.Filled.Devices, "Devices", "${state.sessions.size} active sessions") {}
|
||||
SettingsRow(Icons.Filled.BatterySaver, "Power saving", "Animation and media limits") {}
|
||||
SettingsRow(Icons.Filled.Language, "Language", "English") {}
|
||||
}
|
||||
OutlinedButton(onClick = viewModel::refreshRecoveryStatus, enabled = !state.isSaving) {
|
||||
Text("Refresh status")
|
||||
|
||||
SettingsSectionCard {
|
||||
Text("Appearance", style = MaterialTheme.typography.titleSmall, color = Color.White)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ThemeButton(
|
||||
text = "Light",
|
||||
selected = nightMode == AppCompatDelegate.MODE_NIGHT_NO,
|
||||
) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_NO
|
||||
}
|
||||
ThemeButton(
|
||||
text = "Dark",
|
||||
selected = nightMode == AppCompatDelegate.MODE_NIGHT_YES,
|
||||
) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_YES
|
||||
}
|
||||
ThemeButton(
|
||||
text = "System",
|
||||
selected = nightMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
|
||||
) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!state.twoFactorSecret.isNullOrBlank()) {
|
||||
Text("Secret: ${state.twoFactorSecret}")
|
||||
if (!state.twoFactorOtpAuthUrl.isNullOrBlank()) {
|
||||
Text("OTP URI: ${state.twoFactorOtpAuthUrl}", style = MaterialTheme.typography.bodySmall)
|
||||
|
||||
SettingsSectionCard {
|
||||
SettingsRow(Icons.Filled.Shield, "Revoke all sessions", "Sign out from all other devices") {
|
||||
viewModel.revokeAllSessions()
|
||||
}
|
||||
SettingsRow(Icons.Filled.Star, "Premium", "Placeholder section") {}
|
||||
SettingsRow(Icons.AutoMirrored.Filled.HelpOutline, "Help", "FAQ and support") {}
|
||||
}
|
||||
}
|
||||
Text("Recovery codes left: ${state.recoveryCodesRemaining ?: "-"}")
|
||||
OutlinedTextField(
|
||||
value = twoFactorCode,
|
||||
onValueChange = { twoFactorCode = it },
|
||||
label = { Text("2FA code") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = { viewModel.enableTwoFactor(twoFactorCode) },
|
||||
enabled = !state.isSaving && twoFactorCode.isNotBlank(),
|
||||
) {
|
||||
Text("Enable 2FA")
|
||||
|
||||
if (!state.message.isNullOrBlank()) {
|
||||
Text(state.message.orEmpty(), color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
if (!state.errorMessage.isNullOrBlank()) {
|
||||
Text(state.errorMessage.orEmpty(), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.disableTwoFactor(twoFactorCode) },
|
||||
enabled = !state.isSaving && twoFactorCode.isNotBlank(),
|
||||
onClick = onBackToChats,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Disable 2FA")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = recoveryRegenerateCode,
|
||||
onValueChange = { recoveryRegenerateCode = it },
|
||||
label = { Text("Code to regenerate recovery codes") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.regenerateRecoveryCodes(recoveryRegenerateCode) },
|
||||
enabled = !state.isSaving && recoveryRegenerateCode.isNotBlank(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Regenerate recovery codes")
|
||||
}
|
||||
if (state.recoveryCodes.isNotEmpty()) {
|
||||
Text("New recovery codes:", style = MaterialTheme.typography.bodyMedium)
|
||||
state.recoveryCodes.forEach { code -> Text(code, style = MaterialTheme.typography.bodySmall) }
|
||||
}
|
||||
|
||||
if (!state.message.isNullOrBlank()) {
|
||||
Text(state.message!!, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
if (!state.errorMessage.isNullOrBlank()) {
|
||||
Text(state.errorMessage!!, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
Spacer(modifier = Modifier.padding(top = 4.dp))
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onOpenProfile,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Open profile")
|
||||
}
|
||||
Button(
|
||||
onClick = onLogout,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Logout")
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
OutlinedButton(onClick = onBackToChats) {
|
||||
Text("Back to chats")
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = onLogout,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Logout")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsSectionCard(content: @Composable ColumnScope.() -> Unit) {
|
||||
Surface(
|
||||
color = SettingsCard,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(Color(0x141F232B))
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF2A2F38)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = Color(0xFFB38BFF))
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(title, color = Color.White, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(subtitle, color = SettingsMuted, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
OutlinedButton(onClick = onClick) {
|
||||
Text("Open")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeButton(
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
if (selected) {
|
||||
Button(onClick = onClick) { Text(text) }
|
||||
} else {
|
||||
OutlinedButton(onClick = onClick) { Text(text) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user