Improve image preview navigation
This commit is contained in:
@@ -22,6 +22,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.rememberTransformableState
|
||||
import androidx.compose.foundation.gestures.transformable
|
||||
@@ -247,13 +248,29 @@ private fun QMaxApp(vm: QMaxViewModel) {
|
||||
}
|
||||
|
||||
state.previewImageUrl?.let { url ->
|
||||
val previewAttachment = remember(
|
||||
url,
|
||||
state.messages,
|
||||
state.cachedImagePaths,
|
||||
state.session?.serverUrl
|
||||
) {
|
||||
findPreviewAttachment(
|
||||
url = url,
|
||||
messages = state.messages,
|
||||
cachedImagePaths = state.cachedImagePaths,
|
||||
session = state.session
|
||||
)
|
||||
}
|
||||
ImagePreview(
|
||||
url = url,
|
||||
session = state.session,
|
||||
attachment = previewAttachment,
|
||||
position = state.previewImageIndex,
|
||||
total = state.previewImageGallery.size,
|
||||
onPrevious = { vm.moveImagePreview(-1) },
|
||||
onNext = { vm.moveImagePreview(1) },
|
||||
onShare = previewAttachment?.let { attachment -> { vm.shareAttachment(attachment) } },
|
||||
onOpen = previewAttachment?.let { attachment -> { vm.openAttachment(attachment) } },
|
||||
onClose = vm::closeImage
|
||||
)
|
||||
}
|
||||
@@ -3877,12 +3894,16 @@ private fun avatarColor(title: String): Color {
|
||||
private fun ImagePreview(
|
||||
url: String,
|
||||
session: QMaxSession?,
|
||||
attachment: AttachmentDto?,
|
||||
position: Int,
|
||||
total: Int,
|
||||
onPrevious: () -> Unit,
|
||||
onNext: () -> Unit,
|
||||
onShare: (() -> Unit)?,
|
||||
onOpen: (() -> Unit)?,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
BackHandler(onBack = onClose)
|
||||
val model = when {
|
||||
url.isLocalImagePath() -> File(url.removePrefix("file://"))
|
||||
session != null -> imageRequest(url, session.accessToken)
|
||||
@@ -3890,16 +3911,43 @@ private fun ImagePreview(
|
||||
}
|
||||
var scale by remember(url) { mutableStateOf(1f) }
|
||||
var offset by remember(url) { mutableStateOf(Offset.Zero) }
|
||||
var horizontalDrag by remember(url) { mutableStateOf(0f) }
|
||||
val transformState = rememberTransformableState { zoomChange, panChange, _ ->
|
||||
val nextScale = (scale * zoomChange).coerceIn(1f, 5f)
|
||||
scale = nextScale
|
||||
offset = if (nextScale == 1f) Offset.Zero else offset + panChange
|
||||
}
|
||||
fun resetZoom() {
|
||||
scale = 1f
|
||||
offset = Offset.Zero
|
||||
}
|
||||
fun move(delta: Int) {
|
||||
resetZoom()
|
||||
if (delta < 0) onPrevious() else onNext()
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black),
|
||||
.background(Color.Black)
|
||||
.pointerInput(url, total, scale) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = { horizontalDrag = 0f },
|
||||
onHorizontalDrag = { _, dragAmount ->
|
||||
if (total > 1 && scale <= 1.02f) {
|
||||
horizontalDrag += dragAmount
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
when {
|
||||
horizontalDrag > 120f -> move(-1)
|
||||
horizontalDrag < -120f -> move(1)
|
||||
}
|
||||
horizontalDrag = 0f
|
||||
},
|
||||
onDragCancel = { horizontalDrag = 0f }
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (model != null) {
|
||||
@@ -3930,32 +3978,44 @@ private fun ImagePreview(
|
||||
}
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
color = Color(0x99000000),
|
||||
shape = CircleShape,
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(14.dp)
|
||||
.align(Alignment.TopCenter)
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.background(Color(0x99000000))
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onClose, modifier = Modifier.size(44.dp)) {
|
||||
Icon(Icons.Filled.Close, contentDescription = "Закрыть", tint = Color.White)
|
||||
}
|
||||
}
|
||||
if (total > 1) {
|
||||
Surface(
|
||||
color = Color(0x99000000),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 18.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "${position + 1}/$total",
|
||||
text = attachment?.displayFileName() ?: "Фото",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = if (total > 1) "${position + 1} из $total" else "Просмотр",
|
||||
color = Color(0xCCFFFFFF),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
onShare?.let { share ->
|
||||
IconButton(onClick = share, modifier = Modifier.size(44.dp)) {
|
||||
Icon(Icons.Filled.Share, contentDescription = "Поделиться", tint = Color.White)
|
||||
}
|
||||
}
|
||||
onOpen?.let { open ->
|
||||
TextButton(onClick = open) {
|
||||
Text("Открыть", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (total > 1) {
|
||||
Surface(
|
||||
color = Color(0x99000000),
|
||||
shape = CircleShape,
|
||||
@@ -3963,7 +4023,7 @@ private fun ImagePreview(
|
||||
.align(Alignment.CenterStart)
|
||||
.padding(start = 10.dp)
|
||||
) {
|
||||
IconButton(onClick = onPrevious, modifier = Modifier.size(48.dp)) {
|
||||
IconButton(onClick = { move(-1) }, modifier = Modifier.size(48.dp)) {
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, contentDescription = "Previous photo", tint = Color.White)
|
||||
}
|
||||
}
|
||||
@@ -3974,11 +4034,33 @@ private fun ImagePreview(
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 10.dp)
|
||||
) {
|
||||
IconButton(onClick = onNext, modifier = Modifier.size(48.dp)) {
|
||||
IconButton(onClick = { move(1) }, modifier = Modifier.size(48.dp)) {
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Next photo", tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = 18.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Surface(color = Color(0x99000000), shape = RoundedCornerShape(18.dp)) {
|
||||
Text(
|
||||
text = "${(scale * 100).toInt()}%",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 7.dp)
|
||||
)
|
||||
}
|
||||
if (scale > 1.02f || offset != Offset.Zero) {
|
||||
TextButton(onClick = { resetZoom() }) {
|
||||
Text("Сбросить", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4097,6 +4179,33 @@ private fun rememberAttachmentUrl(session: QMaxSession, attachment: AttachmentDt
|
||||
return "${session.serverUrl.trimEnd('/')}${attachment.downloadPath}"
|
||||
}
|
||||
|
||||
private fun findPreviewAttachment(
|
||||
url: String,
|
||||
messages: List<MessageDto>,
|
||||
cachedImagePaths: Map<String, String>,
|
||||
session: QMaxSession?
|
||||
): AttachmentDto? {
|
||||
val normalizedUrl = url.removePrefix("file://")
|
||||
val serverUrl = session?.serverUrl?.trimEnd('/')
|
||||
return messages
|
||||
.flatMap { it.attachments }
|
||||
.firstOrNull { attachment ->
|
||||
val candidates = buildList {
|
||||
add(attachment.downloadPath)
|
||||
if (serverUrl != null) {
|
||||
add("$serverUrl${attachment.downloadPath}")
|
||||
}
|
||||
cachedImagePaths[attachment.id]?.let { cached ->
|
||||
add(cached)
|
||||
add("file://$cached")
|
||||
}
|
||||
}
|
||||
candidates.any { candidate ->
|
||||
candidate == url || candidate.removePrefix("file://") == normalizedUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberAuthorizedPlayer(url: String, token: String): ExoPlayer {
|
||||
val context = LocalContext.current
|
||||
|
||||
Reference in New Issue
Block a user