Приложение
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user