Приложение
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
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(profileId) {
|
||||
viewModel.loadProfile(profileId)
|
||||
}
|
||||
|
||||
val profile = state.profile
|
||||
val isOwnProfile = profileId == null
|
||||
|
||||
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)) },
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
item {
|
||||
ProfileHeader(
|
||||
username = profile?.username ?: "",
|
||||
displayName = profile?.displayName,
|
||||
avatarUrl = profile?.avatarUrl,
|
||||
bio = profile?.bio,
|
||||
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?,
|
||||
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) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user