Files
forkmessager/client-mobile/core/presentation/components/AppMediaLightbox.kt
Халимов Рустам f17edfc0da Зум и пролистывание
2026-04-15 02:12:43 +03:00

242 lines
11 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 core.presentation.components
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.calculatePan
import androidx.compose.foundation.gestures.calculateZoom
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.InsertDriveFile
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.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import chats.domain.model.Media
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import android.content.Intent
import android.net.Uri
import kotlinx.coroutines.launch
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable
fun AppMediaLightbox(
mediaList: List<Media>,
initialIndex: Int = 0,
onClose: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(initialPage = initialIndex, pageCount = { mediaList.size })
var canScroll by remember { mutableStateOf(true) }
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
pageSpacing = 16.dp,
beyondBoundsPageCount = 1,
userScrollEnabled = canScroll
) { page ->
val media = mediaList[page]
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (media.type.startsWith("video")) {
val isCurrentPage = pagerState.currentPage == page
AppVideoPlayer(
url = media.url,
modifier = Modifier.fillMaxSize(),
useController = true,
autoPlay = isCurrentPage // Только если страница активна
)
} else if (media.type.startsWith("image") || media.type.contains("gif")) {
var scale by remember { mutableFloatStateOf(1f) }
var offset by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) }
// Reset zoom when page changes
LaunchedEffect(pagerState.currentPage) {
scale = 1f
offset = androidx.compose.ui.geometry.Offset.Zero
canScroll = true
}
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
if (scale > 1f) {
scale = 1f
offset = Offset.Zero
canScroll = true
} else {
scale = 3f
canScroll = false
}
}
)
}
.pointerInput(Unit) {
awaitEachGesture {
do {
val event = awaitPointerEvent()
val zoomActive = scale > 1f || event.changes.size > 1
if (zoomActive) {
// Если мы в режиме зума - поглощаем события
event.changes.forEach { it.consume() }
// Сами вычисляем трансформацию
val zoomChange = event.calculateZoom()
val panChange = event.calculatePan()
scale = (scale * zoomChange).coerceIn(1f, 5f)
canScroll = scale <= 1f
if (scale > 1f) {
offset += panChange
} else {
offset = Offset.Zero
}
}
// Если zoomActive == false, мы НЕ вызываем consume(),
// и событие уходит наверх в HorizontalPager
} while (event.changes.any { it.pressed })
}
},
contentAlignment = Alignment.Center
) {
AsyncImage(
model = media.url,
imageLoader = core.utils.CoilUtils.getGifImageLoader(LocalContext.current),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = offset.x,
translationY = offset.y
),
contentScale = ContentScale.Fit
)
}
}
else {
// Document / File placeholder in Lightbox
Column(
modifier = Modifier.fillMaxSize().padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.InsertDriveFile,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(100.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = media.filename ?: "File",
color = Color.White,
style = MaterialTheme.typography.headlineSmall,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.url))
context.startActivity(intent)
} catch (e: Exception) {
// Handle error (no app to open)
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = Color.Black)
) {
Text("Открыть в приложении")
}
}
}
}
}
// Top Bar
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = onClose,
modifier = Modifier
.clip(CircleShape)
.background(Color.Black.copy(alpha = 0.5f))
) {
Icon(Icons.Default.Close, contentDescription = null, tint = Color.White)
}
if (mediaList.isNotEmpty()) {
Text(
text = "${pagerState.currentPage + 1} / ${mediaList.size}",
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
}
IconButton(
onClick = {
val currentMedia = mediaList[pagerState.currentPage]
scope.launch {
core.utils.DownloadUtils.downloadMedia(context, currentMedia.url, currentMedia.filename)
}
},
modifier = Modifier
.clip(CircleShape)
.background(Color.Black.copy(alpha = 0.5f))
) {
Icon(Icons.Default.Download, contentDescription = null, tint = Color.White)
}
}
}
}
}