feat: redesign profile screen with real account actions
Some checks failed
Android CI / android (push) Failing after 4m14s
Android Release / release (push) Failing after 4m40s
CI / test (push) Has been cancelled

This commit is contained in:
2026-04-06 02:13:12 +03:00
parent 67482da0b7
commit 09ddb1ef41
3 changed files with 270 additions and 15 deletions

View File

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

View File

@@ -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>

View File

@@ -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>