Files
forkmessager/client-mobile/stories/presentation/CreateStoryScreen.kt
Халимов Рустам 1fb1be47dd Приложение
2026-04-14 01:15:54 +03:00

317 lines
10 KiB
Kotlin

package stories.presentation
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
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 androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import coil.compose.AsyncImage
import core.presentation.theme.OnSurfaceVariant
import core.presentation.theme.PrimaryIndigo
import core.presentation.theme.SurfaceContainer
import ru.knot.messager.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CreateStoryScreen(
onClose: () -> Unit,
onCreated: () -> Unit,
viewModel: CreateStoryViewModel = hiltViewModel()
) {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
val isVideo = context.contentResolver.getType(it)?.startsWith("video/") == true
viewModel.onMediaSelected(it, isVideo)
}
}
LaunchedEffect(viewModel.uploadSuccess) {
viewModel.uploadSuccess.collect { success ->
if (success) {
onCreated()
onClose()
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
Column(modifier = Modifier.fillMaxSize()) {
// Header
Header(
onClose = onClose,
isUploading = viewModel.isUploading,
onPublish = { viewModel.publishStory(context) }
)
Row(modifier = Modifier.weight(1f)) {
// Main Area
Box(
modifier = Modifier
.weight(1f)
.padding(16.dp)
.clip(RoundedCornerShape(32.dp))
.background(viewModel.bgColor)
.border(8.dp, Color(0xFF161616), RoundedCornerShape(32.dp)),
contentAlignment = Alignment.Center
) {
if (viewModel.selectedMediaUri == null) {
EmptyMediaState(onSelect = { launcher.launch("*/*") })
} else {
MediaPreview(
uri = viewModel.selectedMediaUri,
isVideo = viewModel.isVideo,
onRemove = { viewModel.removeMedia() }
)
}
}
// Sidebar Tools
Sidebar(
currentTool = viewModel.currentTool,
onToolSelect = { viewModel.setTool(it) },
onAddText = { viewModel.addText("New Text") },
onColorChange = { viewModel.updateBgColor(it) },
bgColor = viewModel.bgColor
)
}
}
}
}
@Composable
fun Header(
onClose: () -> Unit,
isUploading: Boolean,
onPublish: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.background(Color(0xFF0A0A0A))
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onClose) {
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.back), tint = Color.Gray)
}
Spacer(modifier = Modifier.width(8.dp))
Text(
stringResource(R.string.story_editor),
style = MaterialTheme.typography.labelMedium.copy(
fontWeight = FontWeight.Black,
letterSpacing = 2.sp,
color = Color.Gray
)
)
}
Button(
onClick = onPublish,
enabled = !isUploading,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryIndigo),
shape = RoundedCornerShape(12.dp)
) {
if (isUploading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text(
stringResource(R.string.publish),
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Black)
)
}
}
}
}
@Composable
fun EmptyMediaState(onSelect: () -> Unit) {
Column(
modifier = Modifier.clickable { onSelect() },
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(80.dp)
.border(2.dp, Color(0xFF333333), CircleShape),
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.CameraAlt, contentDescription = null, tint = Color(0xFF333333), modifier = Modifier.size(32.dp))
}
Spacer(modifier = Modifier.height(16.dp))
Text(
stringResource(R.string.start_creation),
style = MaterialTheme.typography.labelSmall.copy(
fontWeight = FontWeight.Black,
letterSpacing = 4.sp,
color = Color(0xFF333333)
)
)
}
}
@Composable
fun MediaPreview(
uri: Uri?,
isVideo: Boolean,
onRemove: () -> Unit
) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
repeatMode = Player.REPEAT_MODE_ALL
playWhenReady = true
}
}
LaunchedEffect(uri) {
if (isVideo && uri != null) {
exoPlayer.setMediaItem(MediaItem.fromUri(uri))
exoPlayer.prepare()
}
}
DisposableEffect(Unit) {
onDispose { exoPlayer.release() }
}
Box(modifier = Modifier.fillMaxSize()) {
if (isVideo) {
AndroidView(
factory = {
PlayerView(context).apply {
player = exoPlayer
useController = false
resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM
}
},
modifier = Modifier.fillMaxSize()
)
} else {
AsyncImage(
model = uri,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
IconButton(
onClick = onRemove,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(16.dp)
.background(Color.Black.copy(0.6f), RoundedCornerShape(12.dp))
) {
Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.remove), tint = Color.White)
}
}
}
@Composable
fun Sidebar(
currentTool: StoryTool,
onToolSelect: (StoryTool) -> Unit,
onAddText: () -> Unit,
onColorChange: (Color) -> Unit,
bgColor: Color
) {
Column(
modifier = Modifier
.width(100.dp)
.fillMaxHeight()
.background(Color(0xFF0A0A0A))
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
ToolButton(Icons.Default.TextFields, stringResource(R.string.text_tool), currentTool is StoryTool.Text) { onAddText() }
ToolButton(Icons.Default.ContentCut, stringResource(R.string.crop_tool), currentTool is StoryTool.Crop) { onToolSelect(StoryTool.Crop) }
ToolButton(Icons.Default.SentimentSatisfied, stringResource(R.string.stickers_tool), currentTool is StoryTool.Stickers) { onToolSelect(StoryTool.Stickers) }
ToolButton(Icons.Default.Brush, stringResource(R.string.brush_tool), currentTool is StoryTool.Brush) { onToolSelect(StoryTool.Brush) }
ToolButton(Icons.Default.Tune, stringResource(R.string.filters_tool), currentTool is StoryTool.Filters) { onToolSelect(StoryTool.Filters) }
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(bgColor)
.border(2.dp, Color.White.copy(0.1f), CircleShape)
.clickable { onColorChange(Color((0..0xFFFFFF).random() or 0xFF000000.toInt())) }
)
}
}
@Composable
fun ToolButton(
icon: ImageVector,
label: String,
isSelected: Boolean,
onClick: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
icon,
contentDescription = label,
tint = if (isSelected) PrimaryIndigo else OnSurfaceVariant,
modifier = Modifier.size(24.dp)
)
Text(
label,
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 8.sp,
fontWeight = FontWeight.Black,
color = if (isSelected) Color.White else OnSurfaceVariant
)
)
}
}