From 09ddb1ef4140383446f1fd01d6a1d0ba7c72d012 Mon Sep 17 00:00:00 2001 From: benya Date: Mon, 6 Apr 2026 02:13:12 +0300 Subject: [PATCH] feat: redesign profile screen with real account actions --- .../messenger/ui/profile/ProfileScreen.kt | 265 +++++++++++++++++- .../app/src/main/res/values-ru/strings.xml | 10 + android/app/src/main/res/values/strings.xml | 10 + 3 files changed, 270 insertions(+), 15 deletions(-) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt index 38910a0..6587f13 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt @@ -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, + ) + } + } } } diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index dd5371e..77380f1 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -100,6 +100,16 @@ Не указано Редактировать профиль URL аватара + Профиль + Действия + Скопировать юзернейм + Поделиться юзернеймом + Юзернейм скопирован + Скопировать email + Email скопирован + Изменить имя, юзернейм, описание и аватар. + Выбрать и обрезать новое фото профиля. + Изменения сохраняются в аккаунт и используются во всём приложении. Обрезать аватар Предпросмотр обрезки аватара Использовать diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 973a9f4..8b729d9 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -100,6 +100,16 @@ Not set Edit profile Avatar URL + Profile + Actions + Copy username + Share username + Username copied + Copy email + Email copied + Update name, username, bio and avatar. + Choose and crop a new profile photo. + Changes are saved to your account and used across the app. Crop avatar Avatar crop preview Use