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)
|
|
}
|
|
}
|
|
}
|