193 lines
6.6 KiB
Kotlin
193 lines
6.6 KiB
Kotlin
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)
|
||
}
|
||
}
|
||
}
|