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

193 lines
6.6 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package stories.presentation
import android.net.Uri
import androidx.annotation.OptIn
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
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.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import coil.compose.AsyncImage
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(UnstableApi::class)
@Composable
fun StoryViewerScreen(
stories: List<String>, // URL-адреса фото/видео
onClose: () -> Unit
) {
val context = LocalContext.current
var currentIndex by remember { mutableIntStateOf(0) }
val progress = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
var isPaused by remember { mutableStateOf(false) }
val currentUrl = stories[currentIndex]
val isVideo = currentUrl.endsWith(".mp4") || currentUrl.contains("video")
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
repeatMode = Player.REPEAT_MODE_OFF
playWhenReady = true
}
}
DisposableEffect(exoPlayer) {
onDispose {
exoPlayer.release()
}
}
// Подготовка видео при смене индекса
LaunchedEffect(currentIndex) {
if (isVideo) {
exoPlayer.setMediaItem(MediaItem.fromUri(Uri.parse(currentUrl)))
exoPlayer.prepare()
exoPlayer.play()
} else {
exoPlayer.stop()
}
}
// Запуск таймера прогресса
LaunchedEffect(currentIndex, isPaused) {
if (!isPaused) {
val duration = if (isVideo) {
// Ждем пока загрузится длительность видео или используем 15сек по умолчанию
delay(500) // Даем немного времени на загрузку метаданных
if (exoPlayer.duration > 0) exoPlayer.duration else 15000
} else {
5000L // 5 секунд для фото
}
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = duration.toInt(),
easing = LinearEasing
)
)
// Когда прогресс дошел до конца — следующая история
if (currentIndex < stories.size - 1) {
currentIndex++
progress.snapTo(0f)
} else {
onClose()
}
} else {
if (isVideo) exoPlayer.pause() else Unit
}
}
// Возобновление видео при снятии паузы
LaunchedEffect(isPaused) {
if (!isPaused && isVideo) exoPlayer.play()
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
isPaused = true
tryAwaitRelease()
isPaused = false
},
onTap = { offset ->
val screenWidth = size.width
if (offset.x < screenWidth / 3) {
if (currentIndex > 0) {
currentIndex--
scope.launch { progress.snapTo(0f) }
}
} else {
if (currentIndex < stories.size - 1) {
currentIndex++
scope.launch { progress.snapTo(0f) }
} else {
onClose()
}
}
}
)
}
) {
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 = currentUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
// Верхние индикаторы
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, start = 8.dp, end = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
stories.forEachIndexed { index, _ ->
val currentProgress = when {
index < currentIndex -> 1f
index == currentIndex -> progress.value
else -> 0f
}
LinearProgressIndicator(
progress = currentProgress,
modifier = Modifier
.weight(1f)
.height(3.dp)
.clip(RoundedCornerShape(2.dp)),
color = Color.White,
trackColor = Color.White.copy(alpha = 0.3f)
)
}
}
IconButton(
onClick = onClose,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 24.dp, end = 16.dp)
) {
Icon(Icons.Default.Close, contentDescription = "Close", tint = Color.White)
}
}
}