333 lines
13 KiB
Kotlin
333 lines
13 KiB
Kotlin
package profiles.presentation
|
||
|
||
import androidx.compose.foundation.*
|
||
import androidx.compose.foundation.layout.*
|
||
import androidx.compose.foundation.lazy.*
|
||
import androidx.compose.foundation.lazy.grid.*
|
||
import androidx.compose.foundation.pager.HorizontalPager
|
||
import androidx.compose.foundation.pager.rememberPagerState
|
||
import androidx.compose.foundation.shape.CircleShape
|
||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||
import androidx.compose.material.icons.Icons
|
||
import androidx.compose.material.icons.filled.*
|
||
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.layout.ContentScale
|
||
import androidx.compose.ui.res.stringResource
|
||
import androidx.compose.ui.text.font.FontWeight
|
||
import androidx.compose.ui.unit.dp
|
||
import androidx.compose.ui.unit.sp
|
||
import androidx.hilt.navigation.compose.hiltViewModel
|
||
import coil.compose.AsyncImage
|
||
import core.presentation.components.AppAvatar
|
||
import core.presentation.theme.SoftSquareShape
|
||
import kotlinx.coroutines.launch
|
||
import profiles.presentation.viewmodel.ProfileViewModel
|
||
import ru.knot.messager.R
|
||
|
||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||
@Composable
|
||
fun ProfileScreen(
|
||
profileId: String? = null, // null for own profile
|
||
viewModel: ProfileViewModel = hiltViewModel(),
|
||
onEditProfile: () -> Unit = {},
|
||
onSendMessage: (String) -> Unit = {},
|
||
onCall: (String) -> Unit = {},
|
||
onBack: () -> Unit = {}
|
||
) {
|
||
val state by viewModel.state.collectAsState()
|
||
|
||
// Загружаем профиль при первом запуске
|
||
LaunchedEffect(Unit) {
|
||
android.util.Log.d("ProfileScreen", "Loading profile with profileId=$profileId")
|
||
viewModel.loadProfile(profileId)
|
||
}
|
||
|
||
val profile = state.profile
|
||
val isOwnProfile = profileId == null
|
||
|
||
android.util.Log.d("ProfileScreen", "Profile loaded: displayName=${profile?.displayName}, bio=${profile?.bio}, birthday=${profile?.birthday}")
|
||
|
||
val mediaTabs = listOf(
|
||
stringResource(R.string.media),
|
||
"GIF",
|
||
"Файлы",
|
||
"Ссылки"
|
||
)
|
||
|
||
val pagerState = rememberPagerState(pageCount = { mediaTabs.size })
|
||
val coroutineScope = rememberCoroutineScope()
|
||
|
||
if (state.isLoading) {
|
||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||
CircularProgressIndicator()
|
||
}
|
||
return
|
||
}
|
||
|
||
Scaffold(
|
||
topBar = {
|
||
TopAppBar(
|
||
title = { Text(if (isOwnProfile) stringResource(R.string.profile) else stringResource(R.string.profile), fontWeight = FontWeight.Bold) },
|
||
navigationIcon = {
|
||
IconButton(onClick = onBack) {
|
||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(R.string.back))
|
||
}
|
||
},
|
||
actions = {
|
||
if (isOwnProfile) {
|
||
IconButton(onClick = onEditProfile) {
|
||
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.edit_profile))
|
||
}
|
||
}
|
||
},
|
||
colors = TopAppBarDefaults.topAppBarColors(
|
||
containerColor = Color.Transparent,
|
||
titleContentColor = Color.White,
|
||
actionIconContentColor = Color.White,
|
||
navigationIconContentColor = Color.White
|
||
)
|
||
)
|
||
}
|
||
) { paddingValues ->
|
||
LazyColumn(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.padding(paddingValues)
|
||
) {
|
||
item {
|
||
ProfileHeader(
|
||
username = profile?.username ?: "",
|
||
displayName = profile?.displayName,
|
||
avatarUrl = profile?.avatarUrl,
|
||
bio = profile?.bio,
|
||
birthday = profile?.birthday,
|
||
isOwnProfile = isOwnProfile,
|
||
isCallsEnabled = true,
|
||
onSendMessage = { profile?.id?.let { id -> onSendMessage(id) } },
|
||
onCall = { profile?.id?.let { id -> onCall(id) } }
|
||
)
|
||
}
|
||
|
||
stickyHeader {
|
||
Surface(modifier = Modifier.fillMaxWidth()) {
|
||
ScrollableTabRow(
|
||
selectedTabIndex = pagerState.currentPage,
|
||
edgePadding = 16.dp,
|
||
containerColor = MaterialTheme.colorScheme.surface,
|
||
divider = {}
|
||
) {
|
||
mediaTabs.forEachIndexed { index, title ->
|
||
Tab(
|
||
selected = pagerState.currentPage == index,
|
||
onClick = {
|
||
coroutineScope.launch {
|
||
pagerState.animateScrollToPage(index)
|
||
}
|
||
},
|
||
text = { Text(title) }
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Using item with fillParentMaxSize for Pager inside LazyColumn
|
||
// is tricky, but here we render content directly in LazyColumn
|
||
// to maintain scrolling, or use a fixed height.
|
||
// For gestures, we implement HorizontalPager for the content:
|
||
}
|
||
|
||
// Alternative: Use HorizontalPager for the whole content area below header
|
||
// But to keep header scrolling, we need nested scroll or a different approach.
|
||
// Let's implement HorizontalPager for media content items specifically.
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun ProfileHeader(
|
||
username: String,
|
||
displayName: String?,
|
||
avatarUrl: String?,
|
||
bio: String?,
|
||
birthday: String?,
|
||
isOwnProfile: Boolean,
|
||
isCallsEnabled: Boolean,
|
||
onSendMessage: () -> Unit,
|
||
onCall: () -> Unit
|
||
) {
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(24.dp),
|
||
horizontalAlignment = Alignment.CenterHorizontally
|
||
) {
|
||
Box(contentAlignment = Alignment.BottomEnd) {
|
||
AppAvatar(
|
||
url = avatarUrl,
|
||
name = username,
|
||
size = 120.dp
|
||
)
|
||
if (isOwnProfile) {
|
||
FilledIconButton(
|
||
onClick = { /* Выбор фото */ },
|
||
modifier = Modifier.size(32.dp).offset(x = 4.dp, y = 4.dp),
|
||
shape = CircleShape,
|
||
colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||
) {
|
||
Icon(Icons.Default.CameraAlt, contentDescription = null, modifier = Modifier.size(16.dp))
|
||
}
|
||
}
|
||
}
|
||
|
||
Spacer(modifier = Modifier.height(16.dp))
|
||
Text(text = displayName ?: username, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||
Text(text = "@$username", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||
|
||
if (!isOwnProfile) {
|
||
Spacer(modifier = Modifier.height(24.dp))
|
||
Row(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
horizontalArrangement = Arrangement.SpaceEvenly
|
||
) {
|
||
ActionCircleButton(Icons.Default.Message, stringResource(R.string.message), onSendMessage)
|
||
if (isCallsEnabled) {
|
||
ActionCircleButton(Icons.Default.Call, stringResource(R.string.call), onCall)
|
||
}
|
||
ActionCircleButton(Icons.Default.Notifications, stringResource(R.string.notifications), {})
|
||
}
|
||
}
|
||
|
||
if (bio != null || birthday != null) {
|
||
Spacer(modifier = Modifier.height(24.dp))
|
||
Column(modifier = Modifier.fillMaxWidth()) {
|
||
if (birthday != null) {
|
||
Text(text = "Дата рождения", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||
// Форматируем дату из ISO8601 в dd.MM.yyyy
|
||
val formattedBirthday = try {
|
||
val format = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())
|
||
val date = format.parse(birthday)
|
||
val outputFormat = java.text.SimpleDateFormat("dd.MM.yyyy", java.util.Locale.getDefault())
|
||
date?.let { outputFormat.format(it) } ?: birthday
|
||
} catch (e: Exception) {
|
||
birthday
|
||
}
|
||
Text(text = formattedBirthday, style = MaterialTheme.typography.bodyMedium)
|
||
Spacer(modifier = Modifier.height(8.dp))
|
||
}
|
||
if (bio != null) {
|
||
Text(text = stringResource(R.string.bio), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||
Text(text = bio, style = MaterialTheme.typography.bodyMedium)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun LazyListScope.renderMediaContent(tabIndex: Int) {
|
||
// В реальном приложении данные приходят из ViewModel
|
||
// Группировка по месяцам
|
||
val months = listOf("Октябрь 2023", "Сентябрь 2023")
|
||
|
||
months.forEach { month ->
|
||
item {
|
||
Text(
|
||
text = month,
|
||
modifier = Modifier.padding(16.dp, 8.dp),
|
||
style = MaterialTheme.typography.labelMedium,
|
||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||
)
|
||
}
|
||
|
||
when (tabIndex) {
|
||
0 -> renderMediaGrid() // Медиа (сетка 3x3)
|
||
1 -> renderGifs() // GIF
|
||
2 -> renderFiles() // Файлы
|
||
3 -> renderLinks() // Ссылки
|
||
}
|
||
}
|
||
}
|
||
|
||
// Вспомогательные функции для рендеринга контента...
|
||
fun LazyListScope.renderMediaGrid() {
|
||
item {
|
||
// Упрощенная сетка внутри LazyColumn
|
||
Row(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||
repeat(3) {
|
||
Box(
|
||
modifier = Modifier
|
||
.weight(1f)
|
||
.aspectRatio(1f)
|
||
.padding(2.dp)
|
||
.clip(RoundedCornerShape(8.dp))
|
||
.background(Color.Gray.copy(alpha = 0.2f))
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun LazyListScope.renderFiles() {
|
||
items(3) {
|
||
ListItem(
|
||
headlineContent = { Text("Document.pdf") },
|
||
supportingContent = { Text("2.4 MB • 12.10.23") },
|
||
leadingContent = { Icon(Icons.Default.Description, contentDescription = null) }
|
||
)
|
||
}
|
||
}
|
||
|
||
fun LazyListScope.renderLinks() {
|
||
items(2) {
|
||
ListItem(
|
||
headlineContent = { Text("https://github.com/forkmessager") },
|
||
supportingContent = { Text("GitHub - ForkMessager Project") },
|
||
leadingContent = { Icon(Icons.Default.Link, contentDescription = null) }
|
||
)
|
||
}
|
||
}
|
||
|
||
fun LazyListScope.renderGifs() {
|
||
item {
|
||
Row(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||
repeat(2) {
|
||
Box(
|
||
modifier = Modifier
|
||
.weight(1f)
|
||
.height(120.dp)
|
||
.padding(2.dp)
|
||
.clip(RoundedCornerShape(8.dp))
|
||
.background(Color.Gray.copy(alpha = 0.1f)),
|
||
contentAlignment = Alignment.Center
|
||
) {
|
||
Text("GIF", style = MaterialTheme.typography.labelSmall)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun ActionCircleButton(
|
||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||
label: String,
|
||
onClick: () -> Unit,
|
||
color: Color = MaterialTheme.colorScheme.primary
|
||
) {
|
||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||
FilledIconButton(
|
||
onClick = onClick,
|
||
modifier = Modifier.size(56.dp),
|
||
colors = IconButtonDefaults.filledIconButtonColors(containerColor = color.copy(alpha = 0.1f))
|
||
) {
|
||
Icon(icon, contentDescription = label, tint = color)
|
||
}
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
Text(text = label, style = MaterialTheme.typography.labelSmall)
|
||
}
|
||
}
|