Релиз 1.1.8: fullscreen фото, пересылка и статусы участников групп
This commit is contained in:
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Rosetta versioning — bump here on each release
|
// Rosetta versioning — bump here on each release
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val rosettaVersionName = "1.1.7"
|
val rosettaVersionName = "1.1.8"
|
||||||
val rosettaVersionCode = 19 // Increment on each release
|
val rosettaVersionCode = 20 // Increment on each release
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.rosetta.messenger"
|
namespace = "com.rosetta.messenger"
|
||||||
|
|||||||
@@ -17,14 +17,22 @@ object ReleaseNotes {
|
|||||||
val RELEASE_NOTICE = """
|
val RELEASE_NOTICE = """
|
||||||
Update v$VERSION_PLACEHOLDER
|
Update v$VERSION_PLACEHOLDER
|
||||||
|
|
||||||
Уведомления
|
Полноэкранное фото из медиапикера
|
||||||
- Исправлена регистрация push-токена после переподключений
|
- Переработан fullscreen-оверлей: фото открывается поверх чата и перекрывает интерфейс
|
||||||
- Добавлен fallback для нестандартных payload, чтобы push-уведомления не терялись
|
- Добавлены свайпы влево/вправо для перехода по фото внутри выбранной галереи
|
||||||
- Улучшена отправка push-токена сразу после получения FCM токена
|
- Добавлено закрытие свайпом вверх/вниз с плавной анимацией
|
||||||
|
- Убраны рывки, мигание и лишнее уменьшение фото при перелистывании
|
||||||
|
|
||||||
Интерфейс
|
Редактирование и отправка
|
||||||
- Улучшено поведение сворачивания приложения в стиле Telegram
|
- Инструменты редактирования фото перенесены в полноэкранный оверлей медиапикера
|
||||||
- Стабилизировано отображение нижней системной панели навигации
|
- Улучшена пересылка фото через optimistic UI: сообщение отображается сразу
|
||||||
|
- Исправлена множественная пересылка сообщений, включая сценарий после смены forwarding options
|
||||||
|
- Исправлено копирование пересланных сообщений: теперь корректно копируется текст forward/reply
|
||||||
|
|
||||||
|
Группы
|
||||||
|
- В списках участников групп отображается только статус online/offline
|
||||||
|
- На экране создания группы у текущего пользователя статус отображается как online
|
||||||
|
- Поиск участников по username сохранен
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
fun getNotice(version: String): String =
|
fun getNotice(version: String): String =
|
||||||
|
|||||||
@@ -138,6 +138,28 @@ private data class IncomingRunAvatarUiState(
|
|||||||
val overlays: List<IncomingRunAvatarOverlay>
|
val overlays: List<IncomingRunAvatarOverlay>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun extractCopyableMessageText(message: ChatMessage): String {
|
||||||
|
val directText = message.text.trim()
|
||||||
|
if (directText.isNotEmpty()) {
|
||||||
|
return directText
|
||||||
|
}
|
||||||
|
|
||||||
|
val forwardedText =
|
||||||
|
message.forwardedMessages
|
||||||
|
.mapNotNull { forwarded -> forwarded.text.trim().takeIf { it.isNotEmpty() } }
|
||||||
|
.joinToString("\n\n")
|
||||||
|
if (forwardedText.isNotEmpty()) {
|
||||||
|
return forwardedText
|
||||||
|
}
|
||||||
|
|
||||||
|
val replyText = message.replyData?.text?.trim().orEmpty()
|
||||||
|
if (replyText.isNotEmpty()) {
|
||||||
|
return replyText
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
androidx.compose.foundation.ExperimentalFoundationApi::class,
|
androidx.compose.foundation.ExperimentalFoundationApi::class,
|
||||||
@@ -297,6 +319,8 @@ fun ChatDetailScreen(
|
|||||||
var simplePickerPreviewUri by remember { mutableStateOf<Uri?>(null) }
|
var simplePickerPreviewUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
var simplePickerPreviewSourceThumb by remember { mutableStateOf<ThumbnailPosition?>(null) }
|
var simplePickerPreviewSourceThumb by remember { mutableStateOf<ThumbnailPosition?>(null) }
|
||||||
var simplePickerPreviewCaption by remember { mutableStateOf("") }
|
var simplePickerPreviewCaption by remember { mutableStateOf("") }
|
||||||
|
var simplePickerPreviewGalleryUris by remember { mutableStateOf<List<Uri>>(emptyList()) }
|
||||||
|
var simplePickerPreviewInitialIndex by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
// 🎨 Управление статус баром — ВСЕГДА чёрные иконки в светлой теме
|
// 🎨 Управление статус баром — ВСЕГДА чёрные иконки в светлой теме
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
@@ -1205,30 +1229,41 @@ fun ChatDetailScreen(
|
|||||||
{ it.id }
|
{ it.id }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.joinToString(
|
.mapNotNull {
|
||||||
"\n\n"
|
|
||||||
) {
|
|
||||||
msg
|
msg
|
||||||
->
|
->
|
||||||
val time =
|
val messageText =
|
||||||
SimpleDateFormat(
|
extractCopyableMessageText(
|
||||||
"HH:mm",
|
msg
|
||||||
Locale.getDefault()
|
)
|
||||||
)
|
if (messageText.isBlank()) {
|
||||||
.format(
|
null
|
||||||
msg.timestamp
|
} else {
|
||||||
)
|
val time =
|
||||||
"[${if (msg.isOutgoing) "You" else chatTitle}] $time\n${msg.text}"
|
SimpleDateFormat(
|
||||||
|
"HH:mm",
|
||||||
|
Locale.getDefault()
|
||||||
|
)
|
||||||
|
.format(
|
||||||
|
msg.timestamp
|
||||||
|
)
|
||||||
|
"[${if (msg.isOutgoing) "You" else chatTitle}] $time\n$messageText"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
clipboardManager
|
.joinToString(
|
||||||
.setText(
|
"\n\n"
|
||||||
androidx.compose
|
)
|
||||||
.ui
|
if (textToCopy.isNotBlank()) {
|
||||||
.text
|
clipboardManager
|
||||||
.AnnotatedString(
|
.setText(
|
||||||
textToCopy
|
androidx.compose
|
||||||
)
|
.ui
|
||||||
)
|
.text
|
||||||
|
.AnnotatedString(
|
||||||
|
textToCopy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
selectedMessages =
|
selectedMessages =
|
||||||
emptySet()
|
emptySet()
|
||||||
}
|
}
|
||||||
@@ -2788,7 +2823,7 @@ fun ChatDetailScreen(
|
|||||||
// 💬 Context menu anchored to this bubble
|
// 💬 Context menu anchored to this bubble
|
||||||
if (showContextMenu && contextMenuMessage?.id == message.id) {
|
if (showContextMenu && contextMenuMessage?.id == message.id) {
|
||||||
val msg = contextMenuMessage!!
|
val msg = contextMenuMessage!!
|
||||||
MessageContextMenu(
|
MessageContextMenu(
|
||||||
expanded = true,
|
expanded = true,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
showContextMenu = false
|
showContextMenu = false
|
||||||
@@ -2797,7 +2832,7 @@ fun ChatDetailScreen(
|
|||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
isPinned = contextMenuIsPinned,
|
isPinned = contextMenuIsPinned,
|
||||||
isOutgoing = msg.isOutgoing,
|
isOutgoing = msg.isOutgoing,
|
||||||
hasText = msg.text.isNotBlank(),
|
hasText = extractCopyableMessageText(msg).isNotBlank(),
|
||||||
isSystemAccount = isSystemAccount,
|
isSystemAccount = isSystemAccount,
|
||||||
onReply = {
|
onReply = {
|
||||||
viewModel.setReplyMessages(listOf(msg))
|
viewModel.setReplyMessages(listOf(msg))
|
||||||
@@ -2806,7 +2841,7 @@ fun ChatDetailScreen(
|
|||||||
},
|
},
|
||||||
onCopy = {
|
onCopy = {
|
||||||
clipboardManager.setText(
|
clipboardManager.setText(
|
||||||
androidx.compose.ui.text.AnnotatedString(msg.text)
|
androidx.compose.ui.text.AnnotatedString(extractCopyableMessageText(msg))
|
||||||
)
|
)
|
||||||
showContextMenu = false
|
showContextMenu = false
|
||||||
contextMenuMessage = null
|
contextMenuMessage = null
|
||||||
@@ -3060,7 +3095,7 @@ fun ChatDetailScreen(
|
|||||||
viewModel.sendAvatarMessage()
|
viewModel.sendAvatarMessage()
|
||||||
},
|
},
|
||||||
recipientName = user.title,
|
recipientName = user.title,
|
||||||
onPhotoPreviewRequested = { uri, sourceThumb ->
|
onPhotoPreviewRequested = { uri, sourceThumb, galleryUris, initialIndex ->
|
||||||
hideInputOverlays()
|
hideInputOverlays()
|
||||||
showMediaPicker = false
|
showMediaPicker = false
|
||||||
showContextMenu = false
|
showContextMenu = false
|
||||||
@@ -3068,6 +3103,14 @@ fun ChatDetailScreen(
|
|||||||
simplePickerPreviewSourceThumb = sourceThumb
|
simplePickerPreviewSourceThumb = sourceThumb
|
||||||
simplePickerPreviewCaption = ""
|
simplePickerPreviewCaption = ""
|
||||||
simplePickerPreviewUri = uri
|
simplePickerPreviewUri = uri
|
||||||
|
val normalizedGallery =
|
||||||
|
if (galleryUris.isNotEmpty()) galleryUris else listOf(uri)
|
||||||
|
simplePickerPreviewGalleryUris = normalizedGallery
|
||||||
|
simplePickerPreviewInitialIndex =
|
||||||
|
initialIndex.coerceIn(
|
||||||
|
0,
|
||||||
|
(normalizedGallery.size - 1).coerceAtLeast(0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -3113,7 +3156,7 @@ fun ChatDetailScreen(
|
|||||||
viewModel.sendAvatarMessage()
|
viewModel.sendAvatarMessage()
|
||||||
},
|
},
|
||||||
recipientName = user.title,
|
recipientName = user.title,
|
||||||
onPhotoPreviewRequested = { uri, sourceThumb ->
|
onPhotoPreviewRequested = { uri, sourceThumb, galleryUris, initialIndex ->
|
||||||
hideInputOverlays()
|
hideInputOverlays()
|
||||||
showMediaPicker = false
|
showMediaPicker = false
|
||||||
showContextMenu = false
|
showContextMenu = false
|
||||||
@@ -3121,6 +3164,14 @@ fun ChatDetailScreen(
|
|||||||
simplePickerPreviewSourceThumb = sourceThumb
|
simplePickerPreviewSourceThumb = sourceThumb
|
||||||
simplePickerPreviewCaption = ""
|
simplePickerPreviewCaption = ""
|
||||||
simplePickerPreviewUri = uri
|
simplePickerPreviewUri = uri
|
||||||
|
val normalizedGallery =
|
||||||
|
if (galleryUris.isNotEmpty()) galleryUris else listOf(uri)
|
||||||
|
simplePickerPreviewGalleryUris = normalizedGallery
|
||||||
|
simplePickerPreviewInitialIndex =
|
||||||
|
initialIndex.coerceIn(
|
||||||
|
0,
|
||||||
|
(normalizedGallery.size - 1).coerceAtLeast(0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3418,6 +3469,8 @@ fun ChatDetailScreen(
|
|||||||
SimpleFullscreenPhotoOverlay(
|
SimpleFullscreenPhotoOverlay(
|
||||||
imageUri = previewUri,
|
imageUri = previewUri,
|
||||||
sourceThumbnail = simplePickerPreviewSourceThumb,
|
sourceThumbnail = simplePickerPreviewSourceThumb,
|
||||||
|
galleryImageUris = simplePickerPreviewGalleryUris,
|
||||||
|
initialGalleryIndex = simplePickerPreviewInitialIndex,
|
||||||
modifier = Modifier.fillMaxSize().zIndex(100f),
|
modifier = Modifier.fillMaxSize().zIndex(100f),
|
||||||
showCaptionInput = true,
|
showCaptionInput = true,
|
||||||
caption = simplePickerPreviewCaption,
|
caption = simplePickerPreviewCaption,
|
||||||
@@ -3429,12 +3482,16 @@ fun ChatDetailScreen(
|
|||||||
simplePickerPreviewUri = null
|
simplePickerPreviewUri = null
|
||||||
simplePickerPreviewSourceThumb = null
|
simplePickerPreviewSourceThumb = null
|
||||||
simplePickerPreviewCaption = ""
|
simplePickerPreviewCaption = ""
|
||||||
|
simplePickerPreviewGalleryUris = emptyList()
|
||||||
|
simplePickerPreviewInitialIndex = 0
|
||||||
inputFocusTrigger++
|
inputFocusTrigger++
|
||||||
},
|
},
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
simplePickerPreviewUri = null
|
simplePickerPreviewUri = null
|
||||||
simplePickerPreviewSourceThumb = null
|
simplePickerPreviewSourceThumb = null
|
||||||
simplePickerPreviewCaption = ""
|
simplePickerPreviewCaption = ""
|
||||||
|
simplePickerPreviewGalleryUris = emptyList()
|
||||||
|
simplePickerPreviewInitialIndex = 0
|
||||||
inputFocusTrigger++
|
inputFocusTrigger++
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -737,11 +737,7 @@ fun GroupInfoScreen(
|
|||||||
info?.title?.takeIf { it.isNotBlank() }
|
info?.title?.takeIf { it.isNotBlank() }
|
||||||
?: info?.username?.takeIf { it.isNotBlank() }
|
?: info?.username?.takeIf { it.isNotBlank() }
|
||||||
?: fallbackName
|
?: fallbackName
|
||||||
val subtitle = when {
|
val subtitle = if (isOnline) "online" else "offline"
|
||||||
isOnline -> "online"
|
|
||||||
info?.username?.isNotBlank() == true -> "@${info.username}"
|
|
||||||
else -> key.take(18)
|
|
||||||
}
|
|
||||||
GroupMemberUi(
|
GroupMemberUi(
|
||||||
publicKey = key,
|
publicKey = key,
|
||||||
title = displayTitle,
|
title = displayTitle,
|
||||||
@@ -761,9 +757,11 @@ fun GroupInfoScreen(
|
|||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
val username = memberInfoByKey[member.publicKey]?.username?.lowercase().orEmpty()
|
||||||
member.title.lowercase().contains(query) ||
|
member.title.lowercase().contains(query) ||
|
||||||
member.subtitle.lowercase().contains(query) ||
|
member.subtitle.lowercase().contains(query) ||
|
||||||
member.publicKey.lowercase().contains(query)
|
member.publicKey.lowercase().contains(query) ||
|
||||||
|
username.contains(query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ fun GroupSetupScreen(
|
|||||||
.ifBlank { normalizedUsername }
|
.ifBlank { normalizedUsername }
|
||||||
.ifBlank { shortPublicKey(accountPublicKey) }
|
.ifBlank { shortPublicKey(accountPublicKey) }
|
||||||
}
|
}
|
||||||
val selfSubtitle = if (normalizedUsername.isNotBlank()) "@$normalizedUsername" else "you"
|
val selfSubtitle = "online"
|
||||||
|
|
||||||
fun openGroup(dialogPublicKey: String, groupTitle: String) {
|
fun openGroup(dialogPublicKey: String, groupTitle: String) {
|
||||||
onGroupOpened(
|
onGroupOpened(
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ fun ChatAttachAlert(
|
|||||||
currentUserPublicKey: String = "",
|
currentUserPublicKey: String = "",
|
||||||
maxSelection: Int = 10,
|
maxSelection: Int = 10,
|
||||||
recipientName: String? = null,
|
recipientName: String? = null,
|
||||||
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?) -> Unit)? = null,
|
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?, List<Uri>, Int) -> Unit)? = null,
|
||||||
viewModel: AttachAlertViewModel = viewModel()
|
viewModel: AttachAlertViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -1057,7 +1057,13 @@ fun ChatAttachAlert(
|
|||||||
if (!item.isVideo) {
|
if (!item.isVideo) {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
if (onPhotoPreviewRequested != null) {
|
if (onPhotoPreviewRequested != null) {
|
||||||
onPhotoPreviewRequested(item.uri, position)
|
val photoItems = state.visibleMediaItems.filter { !it.isVideo }
|
||||||
|
val photoUris = photoItems.map { it.uri }
|
||||||
|
val currentIndex =
|
||||||
|
photoItems.indexOfFirst { it.id == item.id }
|
||||||
|
.takeIf { it >= 0 }
|
||||||
|
?: photoUris.indexOf(item.uri).coerceAtLeast(0)
|
||||||
|
onPhotoPreviewRequested(item.uri, position, photoUris, currentIndex)
|
||||||
} else {
|
} else {
|
||||||
thumbnailPosition = position
|
thumbnailPosition = position
|
||||||
viewModel.setEditingItem(item)
|
viewModel.setEditingItem(item)
|
||||||
@@ -1260,9 +1266,17 @@ fun ChatAttachAlert(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
state.editingItem?.let { item ->
|
state.editingItem?.let { item ->
|
||||||
|
val galleryPhotoItems = state.visibleMediaItems.filter { !it.isVideo }
|
||||||
|
val galleryPhotoUris = galleryPhotoItems.map { it.uri }
|
||||||
|
val initialGalleryIndex =
|
||||||
|
galleryPhotoItems.indexOfFirst { it.id == item.id }
|
||||||
|
.takeIf { it >= 0 }
|
||||||
|
?: galleryPhotoUris.indexOf(item.uri).coerceAtLeast(0)
|
||||||
SimpleFullscreenPhotoViewer(
|
SimpleFullscreenPhotoViewer(
|
||||||
imageUri = item.uri,
|
imageUri = item.uri,
|
||||||
sourceThumbnail = thumbnailPosition,
|
sourceThumbnail = thumbnailPosition,
|
||||||
|
galleryImageUris = galleryPhotoUris,
|
||||||
|
initialGalleryIndex = initialGalleryIndex,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
viewModel.setEditingItem(null)
|
viewModel.setEditingItem(null)
|
||||||
thumbnailPosition = null
|
thumbnailPosition = null
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ fun MediaPickerBottomSheet(
|
|||||||
currentUserPublicKey: String = "",
|
currentUserPublicKey: String = "",
|
||||||
maxSelection: Int = 10,
|
maxSelection: Int = 10,
|
||||||
recipientName: String? = null,
|
recipientName: String? = null,
|
||||||
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?) -> Unit)? = null
|
onPhotoPreviewRequested: ((Uri, ThumbnailPosition?, List<Uri>, Int) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val hasNativeNavigationBar = remember(context) {
|
val hasNativeNavigationBar = remember(context) {
|
||||||
@@ -1043,7 +1043,13 @@ fun MediaPickerBottomSheet(
|
|||||||
if (!item.isVideo) {
|
if (!item.isVideo) {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
if (onPhotoPreviewRequested != null) {
|
if (onPhotoPreviewRequested != null) {
|
||||||
onPhotoPreviewRequested(item.uri, position)
|
val photoItems = visibleMediaItems.filter { !it.isVideo }
|
||||||
|
val photoUris = photoItems.map { it.uri }
|
||||||
|
val currentIndex =
|
||||||
|
photoItems.indexOfFirst { it.id == item.id }
|
||||||
|
.takeIf { it >= 0 }
|
||||||
|
?: photoUris.indexOf(item.uri).coerceAtLeast(0)
|
||||||
|
onPhotoPreviewRequested(item.uri, position, photoUris, currentIndex)
|
||||||
} else {
|
} else {
|
||||||
thumbnailPosition = position
|
thumbnailPosition = position
|
||||||
editingItem = item
|
editingItem = item
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ import androidx.compose.animation.fadeIn
|
|||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.CubicBezierEasing
|
import androidx.compose.animation.core.CubicBezierEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -51,6 +54,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -67,6 +71,7 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
@@ -79,6 +84,9 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import androidx.compose.ui.window.DialogWindowProvider
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||||
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
||||||
import com.rosetta.messenger.ui.components.AppleEmojiEditTextView
|
import com.rosetta.messenger.ui.components.AppleEmojiEditTextView
|
||||||
@@ -95,6 +103,8 @@ import kotlinx.coroutines.flow.collect
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private val ViewerExpandEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
|
private val ViewerExpandEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
|
||||||
|
private val ViewerHorizontalSwipeEasing = CubicBezierEasing(0.22f, 1.0f, 0.36f, 1.0f)
|
||||||
|
private const val ViewerSwipeSettleDurationMs = 220
|
||||||
|
|
||||||
private data class SimpleViewerTransform(
|
private data class SimpleViewerTransform(
|
||||||
val scaleX: Float,
|
val scaleX: Float,
|
||||||
@@ -104,6 +114,11 @@ private data class SimpleViewerTransform(
|
|||||||
val cornerRadiusDp: Float
|
val cornerRadiusDp: Float
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private enum class SwipeAxis {
|
||||||
|
HORIZONTAL,
|
||||||
|
VERTICAL
|
||||||
|
}
|
||||||
|
|
||||||
private fun lerpFloat(start: Float, stop: Float, fraction: Float): Float {
|
private fun lerpFloat(start: Float, stop: Float, fraction: Float): Float {
|
||||||
return start + (stop - start) * fraction
|
return start + (stop - start) * fraction
|
||||||
}
|
}
|
||||||
@@ -158,6 +173,8 @@ fun SimpleFullscreenPhotoViewer(
|
|||||||
imageUri: Uri,
|
imageUri: Uri,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
sourceThumbnail: ThumbnailPosition? = null,
|
sourceThumbnail: ThumbnailPosition? = null,
|
||||||
|
galleryImageUris: List<Uri> = emptyList(),
|
||||||
|
initialGalleryIndex: Int = 0,
|
||||||
showCaptionInput: Boolean = false,
|
showCaptionInput: Boolean = false,
|
||||||
caption: String = "",
|
caption: String = "",
|
||||||
onCaptionChange: ((String) -> Unit)? = null,
|
onCaptionChange: ((String) -> Unit)? = null,
|
||||||
@@ -183,6 +200,8 @@ fun SimpleFullscreenPhotoViewer(
|
|||||||
imageUri = imageUri,
|
imageUri = imageUri,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
sourceThumbnail = sourceThumbnail,
|
sourceThumbnail = sourceThumbnail,
|
||||||
|
galleryImageUris = galleryImageUris,
|
||||||
|
initialGalleryIndex = initialGalleryIndex,
|
||||||
showCaptionInput = showCaptionInput,
|
showCaptionInput = showCaptionInput,
|
||||||
caption = caption,
|
caption = caption,
|
||||||
onCaptionChange = onCaptionChange,
|
onCaptionChange = onCaptionChange,
|
||||||
@@ -197,6 +216,8 @@ fun SimpleFullscreenPhotoOverlay(
|
|||||||
imageUri: Uri,
|
imageUri: Uri,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
sourceThumbnail: ThumbnailPosition? = null,
|
sourceThumbnail: ThumbnailPosition? = null,
|
||||||
|
galleryImageUris: List<Uri> = emptyList(),
|
||||||
|
initialGalleryIndex: Int = 0,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showCaptionInput: Boolean = false,
|
showCaptionInput: Boolean = false,
|
||||||
caption: String = "",
|
caption: String = "",
|
||||||
@@ -208,6 +229,8 @@ fun SimpleFullscreenPhotoOverlay(
|
|||||||
imageUri = imageUri,
|
imageUri = imageUri,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
sourceThumbnail = sourceThumbnail,
|
sourceThumbnail = sourceThumbnail,
|
||||||
|
galleryImageUris = galleryImageUris,
|
||||||
|
initialGalleryIndex = initialGalleryIndex,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
showCaptionInput = showCaptionInput,
|
showCaptionInput = showCaptionInput,
|
||||||
caption = caption,
|
caption = caption,
|
||||||
@@ -222,6 +245,8 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
imageUri: Uri,
|
imageUri: Uri,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
sourceThumbnail: ThumbnailPosition? = null,
|
sourceThumbnail: ThumbnailPosition? = null,
|
||||||
|
galleryImageUris: List<Uri> = emptyList(),
|
||||||
|
initialGalleryIndex: Int = 0,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showCaptionInput: Boolean = false,
|
showCaptionInput: Boolean = false,
|
||||||
caption: String = "",
|
caption: String = "",
|
||||||
@@ -243,7 +268,28 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
var isKeyboardVisible by remember { mutableStateOf(false) }
|
var isKeyboardVisible by remember { mutableStateOf(false) }
|
||||||
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
|
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
|
||||||
var localCaption by remember(imageUri) { mutableStateOf("") }
|
var localCaption by remember(imageUri) { mutableStateOf("") }
|
||||||
var currentImageUri by remember(imageUri) { mutableStateOf(imageUri) }
|
val normalizedGalleryImageUris = remember(imageUri, galleryImageUris) {
|
||||||
|
val filtered = galleryImageUris.distinct()
|
||||||
|
when {
|
||||||
|
filtered.isEmpty() -> listOf(imageUri)
|
||||||
|
filtered.contains(imageUri) -> filtered
|
||||||
|
else -> listOf(imageUri) + filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var currentGalleryIndex by remember(imageUri, normalizedGalleryImageUris, initialGalleryIndex) {
|
||||||
|
val fallbackIndex =
|
||||||
|
normalizedGalleryImageUris.indexOf(imageUri).takeIf { it >= 0 } ?: 0
|
||||||
|
mutableStateOf(initialGalleryIndex.coerceIn(0, normalizedGalleryImageUris.lastIndex.coerceAtLeast(0)).takeIf {
|
||||||
|
normalizedGalleryImageUris.getOrNull(it) != null
|
||||||
|
} ?: fallbackIndex)
|
||||||
|
}
|
||||||
|
var currentImageUri by remember(imageUri, normalizedGalleryImageUris) {
|
||||||
|
mutableStateOf(
|
||||||
|
normalizedGalleryImageUris.getOrNull(currentGalleryIndex)
|
||||||
|
?: normalizedGalleryImageUris.firstOrNull()
|
||||||
|
?: imageUri
|
||||||
|
)
|
||||||
|
}
|
||||||
var currentTool by remember { mutableStateOf(EditorTool.NONE) }
|
var currentTool by remember { mutableStateOf(EditorTool.NONE) }
|
||||||
var selectedColor by remember { mutableStateOf(Color.White) }
|
var selectedColor by remember { mutableStateOf(Color.White) }
|
||||||
var brushSize by remember { mutableStateOf(12f) }
|
var brushSize by remember { mutableStateOf(12f) }
|
||||||
@@ -256,6 +302,13 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
var rotationAngle by remember { mutableStateOf(0f) }
|
var rotationAngle by remember { mutableStateOf(0f) }
|
||||||
var isFlippedHorizontally by remember { mutableStateOf(false) }
|
var isFlippedHorizontally by remember { mutableStateOf(false) }
|
||||||
var isFlippedVertically by remember { mutableStateOf(false) }
|
var isFlippedVertically by remember { mutableStateOf(false) }
|
||||||
|
var swipeAxis by remember { mutableStateOf<SwipeAxis?>(null) }
|
||||||
|
var swipeOffsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var swipeOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
var isSwipeAnimating by remember { mutableStateOf(false) }
|
||||||
|
var isSwitchingImage by remember { mutableStateOf(false) }
|
||||||
|
var editorLoadingUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
var editorCoverUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
val progress = remember(imageUri, sourceThumbnail) {
|
val progress = remember(imageUri, sourceThumbnail) {
|
||||||
Animatable(if (sourceThumbnail != null) 0f else 1f)
|
Animatable(if (sourceThumbnail != null) 0f else 1f)
|
||||||
@@ -293,6 +346,17 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
|
|
||||||
LaunchedEffect(imageUri, sourceThumbnail) {
|
LaunchedEffect(imageUri, sourceThumbnail) {
|
||||||
localCaption = caption
|
localCaption = caption
|
||||||
|
val clampedIndex = initialGalleryIndex.coerceIn(
|
||||||
|
0,
|
||||||
|
normalizedGalleryImageUris.lastIndex.coerceAtLeast(0)
|
||||||
|
)
|
||||||
|
currentGalleryIndex = clampedIndex
|
||||||
|
currentImageUri =
|
||||||
|
normalizedGalleryImageUris.getOrNull(clampedIndex)
|
||||||
|
?: normalizedGalleryImageUris.firstOrNull()
|
||||||
|
?: imageUri
|
||||||
|
editorLoadingUri = null
|
||||||
|
editorCoverUri = null
|
||||||
if (progress.value < 1f) {
|
if (progress.value < 1f) {
|
||||||
progress.animateTo(
|
progress.animateTo(
|
||||||
targetValue = 1f,
|
targetValue = 1f,
|
||||||
@@ -350,6 +414,18 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetEditorStateForImageSwitch() {
|
||||||
|
currentTool = EditorTool.NONE
|
||||||
|
showColorPicker = false
|
||||||
|
isEraserActive = false
|
||||||
|
hasDrawingEdits = false
|
||||||
|
rotationAngle = 0f
|
||||||
|
isFlippedHorizontally = false
|
||||||
|
isFlippedVertically = false
|
||||||
|
photoEditor?.setBrushDrawingMode(false)
|
||||||
|
photoEditor?.clearAllViews()
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleEmojiPicker() {
|
fun toggleEmojiPicker() {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (now - lastToggleTime < toggleCooldownMs) {
|
if (now - lastToggleTime < toggleCooldownMs) {
|
||||||
@@ -380,6 +456,168 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
|
|
||||||
BackHandler { closeViewer() }
|
BackHandler { closeViewer() }
|
||||||
|
|
||||||
|
val canUseSwipeGestures =
|
||||||
|
currentTool == EditorTool.NONE &&
|
||||||
|
!isClosing &&
|
||||||
|
!isSaving &&
|
||||||
|
!showColorPicker &&
|
||||||
|
!isKeyboardVisible &&
|
||||||
|
!showEmojiPicker &&
|
||||||
|
!coordinator.isEmojiBoxVisible
|
||||||
|
val canSwipeHorizontally = canUseSwipeGestures && normalizedGalleryImageUris.size > 1
|
||||||
|
|
||||||
|
val animatedSwipeOffsetX by animateFloatAsState(
|
||||||
|
targetValue = swipeOffsetX,
|
||||||
|
animationSpec =
|
||||||
|
if (isSwipeAnimating) {
|
||||||
|
tween(ViewerSwipeSettleDurationMs, easing = ViewerHorizontalSwipeEasing)
|
||||||
|
} else {
|
||||||
|
snap()
|
||||||
|
},
|
||||||
|
label = "simple_viewer_swipe_x"
|
||||||
|
)
|
||||||
|
val animatedSwipeOffsetY by animateFloatAsState(
|
||||||
|
targetValue = swipeOffsetY,
|
||||||
|
animationSpec = if (isSwipeAnimating) tween(180, easing = ViewerExpandEasing) else snap(),
|
||||||
|
label = "simple_viewer_swipe_y"
|
||||||
|
)
|
||||||
|
|
||||||
|
val isVerticalSwipeActive = swipeAxis == SwipeAxis.VERTICAL || kotlin.math.abs(animatedSwipeOffsetY) > 0.5f
|
||||||
|
val swipeDismissProgress =
|
||||||
|
if (isVerticalSwipeActive && screenSize.height > 0) {
|
||||||
|
(kotlin.math.abs(animatedSwipeOffsetY) / screenSize.height.toFloat()).coerceIn(0f, 1f)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
val swipeScrimAlpha = 1f - swipeDismissProgress * 0.45f
|
||||||
|
|
||||||
|
val swipeGestureModifier =
|
||||||
|
if (canUseSwipeGestures) {
|
||||||
|
Modifier.pointerInput(
|
||||||
|
currentGalleryIndex,
|
||||||
|
normalizedGalleryImageUris,
|
||||||
|
canSwipeHorizontally,
|
||||||
|
screenSize,
|
||||||
|
isSwitchingImage
|
||||||
|
) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = {
|
||||||
|
if (isSwitchingImage) return@detectDragGestures
|
||||||
|
swipeAxis = null
|
||||||
|
isSwipeAnimating = false
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
swipeAxis = null
|
||||||
|
isSwipeAnimating = true
|
||||||
|
swipeOffsetX = 0f
|
||||||
|
swipeOffsetY = 0f
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
val width = screenSize.width.toFloat().coerceAtLeast(1f)
|
||||||
|
val height = screenSize.height.toFloat().coerceAtLeast(1f)
|
||||||
|
val horizontalThreshold = width * 0.2f
|
||||||
|
val verticalThreshold = height * 0.12f
|
||||||
|
|
||||||
|
when (swipeAxis) {
|
||||||
|
SwipeAxis.HORIZONTAL -> {
|
||||||
|
val targetIndex =
|
||||||
|
when {
|
||||||
|
swipeOffsetX <= -horizontalThreshold &&
|
||||||
|
currentGalleryIndex < normalizedGalleryImageUris.lastIndex -> {
|
||||||
|
currentGalleryIndex + 1
|
||||||
|
}
|
||||||
|
swipeOffsetX >= horizontalThreshold && currentGalleryIndex > 0 -> {
|
||||||
|
currentGalleryIndex - 1
|
||||||
|
}
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetIndex >= 0) {
|
||||||
|
val targetUri = normalizedGalleryImageUris[targetIndex]
|
||||||
|
context.imageLoader.enqueue(
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(targetUri)
|
||||||
|
.crossfade(false)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
val toNext = targetIndex > currentGalleryIndex
|
||||||
|
isSwitchingImage = true
|
||||||
|
isSwipeAnimating = true
|
||||||
|
val exitOffset = if (toNext) -width else width
|
||||||
|
swipeOffsetX = exitOffset
|
||||||
|
scope.launch {
|
||||||
|
delay(ViewerSwipeSettleDurationMs.toLong())
|
||||||
|
currentGalleryIndex = targetIndex
|
||||||
|
currentImageUri = targetUri
|
||||||
|
editorCoverUri = targetUri
|
||||||
|
resetEditorStateForImageSwitch()
|
||||||
|
isSwipeAnimating = false
|
||||||
|
swipeOffsetX = 0f
|
||||||
|
delay(90)
|
||||||
|
isSwitchingImage = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isSwipeAnimating = true
|
||||||
|
swipeOffsetX = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SwipeAxis.VERTICAL -> {
|
||||||
|
if (kotlin.math.abs(swipeOffsetY) >= verticalThreshold) {
|
||||||
|
closeViewer()
|
||||||
|
} else {
|
||||||
|
isSwipeAnimating = true
|
||||||
|
swipeOffsetY = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
isSwipeAnimating = true
|
||||||
|
swipeOffsetX = 0f
|
||||||
|
swipeOffsetY = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
swipeAxis = null
|
||||||
|
},
|
||||||
|
onDrag = { change, dragAmount ->
|
||||||
|
if (isSwitchingImage) return@detectDragGestures
|
||||||
|
if (swipeAxis == null) {
|
||||||
|
val absX = kotlin.math.abs(dragAmount.x)
|
||||||
|
val absY = kotlin.math.abs(dragAmount.y)
|
||||||
|
swipeAxis =
|
||||||
|
if (absX > absY && canSwipeHorizontally) {
|
||||||
|
SwipeAxis.HORIZONTAL
|
||||||
|
} else {
|
||||||
|
SwipeAxis.VERTICAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (swipeAxis) {
|
||||||
|
SwipeAxis.HORIZONTAL -> {
|
||||||
|
val width = screenSize.width.toFloat().coerceAtLeast(1f)
|
||||||
|
val atFirst = currentGalleryIndex <= 0
|
||||||
|
val atLast = currentGalleryIndex >= normalizedGalleryImageUris.lastIndex
|
||||||
|
var next = swipeOffsetX + dragAmount.x
|
||||||
|
if ((next > 0f && atFirst) || (next < 0f && atLast)) {
|
||||||
|
next *= 0.32f
|
||||||
|
}
|
||||||
|
swipeOffsetX = next.coerceIn(-width, width)
|
||||||
|
swipeOffsetY = 0f
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
SwipeAxis.VERTICAL -> {
|
||||||
|
val height = screenSize.height.toFloat().coerceAtLeast(1f)
|
||||||
|
swipeOffsetY = (swipeOffsetY + dragAmount.y).coerceIn(-height, height)
|
||||||
|
swipeOffsetX = 0f
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
val transform by remember(sourceThumbnail, screenSize, progress.value) {
|
val transform by remember(sourceThumbnail, screenSize, progress.value) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val p = progress.value
|
val p = progress.value
|
||||||
@@ -418,19 +656,100 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
|
|
||||||
val tapToDismissModifier =
|
val tapToDismissModifier =
|
||||||
if (!showCaptionInput) {
|
if (!showCaptionInput) {
|
||||||
Modifier.pointerInput(imageUri) { detectTapGestures(onTap = { closeViewer() }) }
|
Modifier.pointerInput(currentImageUri) { detectTapGestures(onTap = { closeViewer() }) }
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val horizontalNeighborIndex =
|
||||||
|
remember(currentGalleryIndex, normalizedGalleryImageUris, animatedSwipeOffsetX) {
|
||||||
|
when {
|
||||||
|
animatedSwipeOffsetX < 0f && currentGalleryIndex < normalizedGalleryImageUris.lastIndex ->
|
||||||
|
currentGalleryIndex + 1
|
||||||
|
animatedSwipeOffsetX > 0f && currentGalleryIndex > 0 ->
|
||||||
|
currentGalleryIndex - 1
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val horizontalNeighborUri =
|
||||||
|
normalizedGalleryImageUris.getOrNull(horizontalNeighborIndex)
|
||||||
|
val shouldShowHorizontalNeighbor =
|
||||||
|
horizontalNeighborUri != null && kotlin.math.abs(animatedSwipeOffsetX) > 0.5f
|
||||||
|
val shouldShowHorizontalCurrentLayer =
|
||||||
|
canSwipeHorizontally &&
|
||||||
|
(swipeAxis == SwipeAxis.HORIZONTAL ||
|
||||||
|
isSwitchingImage ||
|
||||||
|
kotlin.math.abs(animatedSwipeOffsetX) > 0.5f ||
|
||||||
|
editorCoverUri != null)
|
||||||
|
val horizontalNeighborTranslationX =
|
||||||
|
if (animatedSwipeOffsetX < 0f) {
|
||||||
|
animatedSwipeOffsetX + screenSize.width.toFloat()
|
||||||
|
} else {
|
||||||
|
animatedSwipeOffsetX - screenSize.width.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
modifier.fillMaxSize()
|
modifier.fillMaxSize()
|
||||||
.onSizeChanged { screenSize = it }
|
.onSizeChanged { screenSize = it }
|
||||||
.background(Color.Black)
|
.background(Color.Black.copy(alpha = swipeScrimAlpha))
|
||||||
.then(tapToDismissModifier),
|
.then(tapToDismissModifier),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
if (shouldShowHorizontalNeighbor) {
|
||||||
|
AsyncImage(
|
||||||
|
model =
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(horizontalNeighborUri)
|
||||||
|
.crossfade(false)
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = transform.scaleX
|
||||||
|
scaleY = transform.scaleY
|
||||||
|
translationX = transform.translationX + horizontalNeighborTranslationX
|
||||||
|
translationY = transform.translationY + animatedSwipeOffsetY
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
if (transform.cornerRadiusDp > 0f) {
|
||||||
|
Modifier.clip(RoundedCornerShape(transform.cornerRadiusDp.dp))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowHorizontalCurrentLayer) {
|
||||||
|
AsyncImage(
|
||||||
|
model =
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(currentImageUri)
|
||||||
|
.crossfade(false)
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier =
|
||||||
|
Modifier.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = transform.scaleX
|
||||||
|
scaleY = transform.scaleY
|
||||||
|
translationX = transform.translationX + animatedSwipeOffsetX
|
||||||
|
translationY = transform.translationY + animatedSwipeOffsetY
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
if (transform.cornerRadiusDp > 0f) {
|
||||||
|
Modifier.clip(RoundedCornerShape(transform.cornerRadiusDp.dp))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
PhotoEditorView(ctx).apply {
|
PhotoEditorView(ctx).apply {
|
||||||
@@ -442,6 +761,7 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
adjustViewBounds = false
|
adjustViewBounds = false
|
||||||
setPadding(0, 0, 0, 0)
|
setPadding(0, 0, 0, 0)
|
||||||
setImageURI(currentImageUri)
|
setImageURI(currentImageUri)
|
||||||
|
tag = currentImageUri
|
||||||
}
|
}
|
||||||
photoEditor = PhotoEditor.Builder(ctx, this)
|
photoEditor = PhotoEditor.Builder(ctx, this)
|
||||||
.setPinchTextScalable(true)
|
.setPinchTextScalable(true)
|
||||||
@@ -451,20 +771,59 @@ private fun SimpleFullscreenPhotoContent(
|
|||||||
},
|
},
|
||||||
update = { editorView ->
|
update = { editorView ->
|
||||||
if (editorView.source.tag != currentImageUri) {
|
if (editorView.source.tag != currentImageUri) {
|
||||||
editorView.source.setImageURI(currentImageUri)
|
if (editorLoadingUri != currentImageUri) {
|
||||||
editorView.source.tag = currentImageUri
|
val requestUri = currentImageUri
|
||||||
|
editorLoadingUri = requestUri
|
||||||
|
context.imageLoader.enqueue(
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(requestUri)
|
||||||
|
.crossfade(false)
|
||||||
|
.target(
|
||||||
|
onSuccess = { result ->
|
||||||
|
if (editorLoadingUri == requestUri) {
|
||||||
|
editorView.source.setImageDrawable(result)
|
||||||
|
editorView.source.tag = requestUri
|
||||||
|
editorLoadingUri = null
|
||||||
|
if (editorCoverUri == requestUri) {
|
||||||
|
editorCoverUri = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError = { error ->
|
||||||
|
if (editorLoadingUri == requestUri) {
|
||||||
|
if (error != null) {
|
||||||
|
editorView.source.setImageDrawable(error)
|
||||||
|
} else {
|
||||||
|
editorView.source.setImageURI(requestUri)
|
||||||
|
}
|
||||||
|
editorView.source.tag = requestUri
|
||||||
|
editorLoadingUri = null
|
||||||
|
if (editorCoverUri == requestUri) {
|
||||||
|
editorCoverUri = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
editorView.source.rotation = rotationAngle
|
editorView.source.rotation = rotationAngle
|
||||||
editorView.source.scaleX = if (isFlippedHorizontally) -1f else 1f
|
editorView.source.scaleX = if (isFlippedHorizontally) -1f else 1f
|
||||||
editorView.source.scaleY = if (isFlippedVertically) -1f else 1f
|
editorView.source.scaleY = if (isFlippedVertically) -1f else 1f
|
||||||
|
if (editorView.source.tag == currentImageUri && editorCoverUri == currentImageUri) {
|
||||||
|
editorCoverUri = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
|
.then(swipeGestureModifier)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
|
alpha = if (shouldShowHorizontalCurrentLayer) 0f else 1f
|
||||||
scaleX = transform.scaleX
|
scaleX = transform.scaleX
|
||||||
scaleY = transform.scaleY
|
scaleY = transform.scaleY
|
||||||
translationX = transform.translationX
|
translationX = transform.translationX + animatedSwipeOffsetX
|
||||||
translationY = transform.translationY
|
translationY = transform.translationY + animatedSwipeOffsetY
|
||||||
}
|
}
|
||||||
.then(
|
.then(
|
||||||
if (transform.cornerRadiusDp > 0f) {
|
if (transform.cornerRadiusDp > 0f) {
|
||||||
|
|||||||
Reference in New Issue
Block a user