feat: redesign profile screen with real account actions
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package ru.daemonlord.messenger.ui.profile
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
@@ -15,6 +17,7 @@ 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.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
@@ -33,10 +36,15 @@ 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.AlternateEmail
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -58,7 +66,9 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
@@ -122,7 +132,11 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||
val profileDisplayName = if (name.isBlank()) stringResource(id = R.string.profile_user_fallback) else name
|
||||
val usernameLabel = username.trim().takeIf { it.isNotBlank() }?.let { "@$it" }
|
||||
val emailLabel = profile?.email?.takeIf { it.isNotBlank() }
|
||||
val pickAvatarLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent(),
|
||||
) { uri ->
|
||||
@@ -192,18 +206,20 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
Text(
|
||||
text = if (name.isBlank()) stringResource(id = R.string.profile_user_fallback) else name,
|
||||
text = profileDisplayName,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
stringResource(id = R.string.chat_status_online),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
usernameLabel?.let {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.82f),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -247,13 +263,139 @@ fun ProfileScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
val notSet = stringResource(id = R.string.profile_not_set)
|
||||
ProfileInfoRow(stringResource(id = R.string.auth_label_email), profile?.email.orEmpty())
|
||||
ProfileInfoRow(stringResource(id = R.string.profile_bio), bio.ifBlank { notSet })
|
||||
ProfileInfoRow(
|
||||
stringResource(id = R.string.auth_label_username),
|
||||
if (username.isBlank()) notSet else "@$username",
|
||||
Text(
|
||||
text = stringResource(id = R.string.profile_about_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
ProfileInfoRow(stringResource(id = R.string.auth_label_name), name.ifBlank { notSet })
|
||||
ProfileInfoRow(
|
||||
label = stringResource(id = R.string.auth_label_name),
|
||||
value = profileDisplayName,
|
||||
)
|
||||
ProfileInfoRow(
|
||||
label = stringResource(id = R.string.auth_label_username),
|
||||
value = usernameLabel ?: notSet,
|
||||
actions = {
|
||||
usernameLabel?.let { handle ->
|
||||
ProfileInlineAction(
|
||||
icon = Icons.Filled.ContentCopy,
|
||||
label = stringResource(id = R.string.profile_copy_username),
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(handle))
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.profile_username_copied),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
ProfileInlineAction(
|
||||
icon = Icons.Filled.Share,
|
||||
label = stringResource(id = R.string.profile_share_username),
|
||||
) {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, handle)
|
||||
}
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.profile_share_username),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
ProfileInfoRow(
|
||||
label = stringResource(id = R.string.auth_label_email),
|
||||
value = emailLabel ?: notSet,
|
||||
actions = {
|
||||
emailLabel?.let { email ->
|
||||
ProfileInlineAction(
|
||||
icon = Icons.Filled.ContentCopy,
|
||||
label = stringResource(id = R.string.profile_copy_email),
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(email))
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.profile_email_copied),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
ProfileInfoRow(
|
||||
label = stringResource(id = R.string.profile_bio),
|
||||
value = bio.ifBlank { notSet },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.offset(y = (-10).dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.profile_actions_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
ProfileActionRow(
|
||||
icon = Icons.Filled.Edit,
|
||||
title = stringResource(id = R.string.profile_edit),
|
||||
subtitle = stringResource(id = R.string.profile_edit_subtitle),
|
||||
) {
|
||||
editMode = true
|
||||
}
|
||||
ProfileActionRow(
|
||||
icon = Icons.Filled.AddAPhoto,
|
||||
title = stringResource(id = R.string.profile_choose_photo),
|
||||
subtitle = stringResource(id = R.string.profile_photo_subtitle),
|
||||
) {
|
||||
pickAvatarLauncher.launch("image/*")
|
||||
}
|
||||
if (usernameLabel != null) {
|
||||
ProfileActionRow(
|
||||
icon = Icons.Filled.AlternateEmail,
|
||||
title = stringResource(id = R.string.profile_share_username),
|
||||
subtitle = usernameLabel,
|
||||
) {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, usernameLabel)
|
||||
}
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.profile_share_username),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (emailLabel != null) {
|
||||
ProfileActionRow(
|
||||
icon = Icons.Filled.Email,
|
||||
title = stringResource(id = R.string.profile_copy_email),
|
||||
subtitle = emailLabel,
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(emailLabel))
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.profile_email_copied),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,6 +446,11 @@ fun ProfileScreen(
|
||||
label = { Text(stringResource(id = R.string.profile_avatar_url)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.profile_edit_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(onClick = { editMode = false }, modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(id = R.string.common_cancel))
|
||||
@@ -392,9 +539,97 @@ private fun HeroActionButton(
|
||||
|
||||
@Composable
|
||||
private fun ProfileInfoRow(label: String, value: String) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(text = value, style = MaterialTheme.typography.titleLarge)
|
||||
Text(text = label, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
ProfileInfoRow(label = label, value = value, actions = {})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileInfoRow(
|
||||
label: String,
|
||||
value: String,
|
||||
actions: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Text(text = value, style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
actions()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileInlineAction(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
IconButton(onClick = onClick) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileActionRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(42.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,16 @@
|
||||
<string name="profile_not_set">Не указано</string>
|
||||
<string name="profile_edit_profile">Редактировать профиль</string>
|
||||
<string name="profile_avatar_url">URL аватара</string>
|
||||
<string name="profile_about_section">Профиль</string>
|
||||
<string name="profile_actions_section">Действия</string>
|
||||
<string name="profile_copy_username">Скопировать юзернейм</string>
|
||||
<string name="profile_share_username">Поделиться юзернеймом</string>
|
||||
<string name="profile_username_copied">Юзернейм скопирован</string>
|
||||
<string name="profile_copy_email">Скопировать email</string>
|
||||
<string name="profile_email_copied">Email скопирован</string>
|
||||
<string name="profile_edit_subtitle">Изменить имя, юзернейм, описание и аватар.</string>
|
||||
<string name="profile_photo_subtitle">Выбрать и обрезать новое фото профиля.</string>
|
||||
<string name="profile_edit_hint">Изменения сохраняются в аккаунт и используются во всём приложении.</string>
|
||||
<string name="profile_crop_avatar">Обрезать аватар</string>
|
||||
<string name="profile_avatar_crop_preview">Предпросмотр обрезки аватара</string>
|
||||
<string name="profile_use_crop">Использовать</string>
|
||||
|
||||
@@ -100,6 +100,16 @@
|
||||
<string name="profile_not_set">Not set</string>
|
||||
<string name="profile_edit_profile">Edit profile</string>
|
||||
<string name="profile_avatar_url">Avatar URL</string>
|
||||
<string name="profile_about_section">Profile</string>
|
||||
<string name="profile_actions_section">Actions</string>
|
||||
<string name="profile_copy_username">Copy username</string>
|
||||
<string name="profile_share_username">Share username</string>
|
||||
<string name="profile_username_copied">Username copied</string>
|
||||
<string name="profile_copy_email">Copy email</string>
|
||||
<string name="profile_email_copied">Email copied</string>
|
||||
<string name="profile_edit_subtitle">Update name, username, bio and avatar.</string>
|
||||
<string name="profile_photo_subtitle">Choose and crop a new profile photo.</string>
|
||||
<string name="profile_edit_hint">Changes are saved to your account and used across the app.</string>
|
||||
<string name="profile_crop_avatar">Crop avatar</string>
|
||||
<string name="profile_avatar_crop_preview">Avatar crop preview</string>
|
||||
<string name="profile_use_crop">Use</string>
|
||||
|
||||
Reference in New Issue
Block a user