fix: close keyboard before dismissing ImageEditorScreen
This commit is contained in:
@@ -2030,6 +2030,18 @@ fun ChatDetailScreen(
|
|||||||
pendingGalleryImages = imageUris
|
pendingGalleryImages = imageUris
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onMediaSelectedWithCaption = { mediaItem, caption ->
|
||||||
|
// 📸 Отправляем фото с caption напрямую
|
||||||
|
showMediaPicker = false
|
||||||
|
scope.launch {
|
||||||
|
val base64 = MediaUtils.uriToBase64Image(context, mediaItem.uri)
|
||||||
|
val blurhash = MediaUtils.generateBlurhash(context, mediaItem.uri)
|
||||||
|
val (width, height) = MediaUtils.getImageDimensions(context, mediaItem.uri)
|
||||||
|
if (base64 != null) {
|
||||||
|
viewModel.sendImageMessage(base64, blurhash, caption, width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onOpenCamera = {
|
onOpenCamera = {
|
||||||
// 📷 Создаём временный файл для фото
|
// 📷 Создаём временный файл для фото
|
||||||
try {
|
try {
|
||||||
@@ -2054,7 +2066,8 @@ fun ChatDetailScreen(
|
|||||||
onAvatarClick = {
|
onAvatarClick = {
|
||||||
// 👤 Отправляем свой аватар (как в desktop)
|
// 👤 Отправляем свой аватар (как в desktop)
|
||||||
viewModel.sendAvatarMessage()
|
viewModel.sendAvatarMessage()
|
||||||
}
|
},
|
||||||
|
recipientName = user.title
|
||||||
)
|
)
|
||||||
|
|
||||||
// 📷 Image Editor для фото с камеры (с caption как в Telegram)
|
// 📷 Image Editor для фото с камеры (с caption как в Telegram)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -38,6 +39,8 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -107,6 +110,8 @@ fun ImageEditorScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val view = LocalView.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
// Editor state
|
// Editor state
|
||||||
var currentTool by remember { mutableStateOf(EditorTool.NONE) }
|
var currentTool by remember { mutableStateOf(EditorTool.NONE) }
|
||||||
@@ -219,9 +224,22 @@ fun ImageEditorScreen(
|
|||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.padding(horizontal = 4.dp, vertical = 8.dp)
|
.padding(horizontal = 4.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
// Close button (X)
|
// Close button (X) - сначала закрывает клавиатуру, потом экран
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onDismiss,
|
onClick = {
|
||||||
|
// Проверяем, открыта ли клавиатура
|
||||||
|
val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager
|
||||||
|
val isKeyboardOpen = imm.isAcceptingText
|
||||||
|
|
||||||
|
if (isKeyboardOpen) {
|
||||||
|
// Закрываем клавиатуру
|
||||||
|
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
|
focusManager.clearFocus()
|
||||||
|
} else {
|
||||||
|
// Закрываем экран
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = Modifier.align(Alignment.CenterStart)
|
modifier = Modifier.align(Alignment.CenterStart)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -340,6 +358,7 @@ fun ImageEditorScreen(
|
|||||||
caption = caption,
|
caption = caption,
|
||||||
onCaptionChange = { caption = it },
|
onCaptionChange = { caption = it },
|
||||||
isSaving = isSaving,
|
isSaving = isSaving,
|
||||||
|
isKeyboardVisible = isKeyboardVisibleForCaption,
|
||||||
onSend = {
|
onSend = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
isSaving = true
|
isSaving = true
|
||||||
@@ -673,32 +692,61 @@ private fun TelegramRotateBar(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Telegram-style caption input bar
|
* Telegram-style caption input bar
|
||||||
|
* Меняет внешний вид в зависимости от состояния клавиатуры:
|
||||||
|
* - Клавиатура закрыта: минимальный стиль (камера + текст + синяя стрелка)
|
||||||
|
* - Клавиатура открыта: полный стиль (emoji + текст + галочка)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun TelegramCaptionBar(
|
private fun TelegramCaptionBar(
|
||||||
caption: String,
|
caption: String,
|
||||||
onCaptionChange: (String) -> Unit,
|
onCaptionChange: (String) -> Unit,
|
||||||
isSaving: Boolean,
|
isSaving: Boolean,
|
||||||
|
isKeyboardVisible: Boolean,
|
||||||
onSend: () -> Unit
|
onSend: () -> Unit
|
||||||
) {
|
) {
|
||||||
// Telegram-style: прямоугольный темный бар с emoji слева и галочкой справа
|
// Анимированный переход между стилями
|
||||||
|
val backgroundAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (isKeyboardVisible) 0.75f else 0f,
|
||||||
|
animationSpec = tween(200, easing = TelegramEasing),
|
||||||
|
label = "background"
|
||||||
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color.Black.copy(alpha = 0.75f))
|
.background(Color.Black.copy(alpha = backgroundAlpha))
|
||||||
.padding(horizontal = 8.dp, vertical = 10.dp),
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
// Emoji icon (как в Telegram)
|
// Левая иконка: камера когда клавиатура закрыта, emoji когда открыта
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = isKeyboardVisible,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(tween(150)) togetherWith fadeOut(tween(150))
|
||||||
|
},
|
||||||
|
label = "left_icon"
|
||||||
|
) { keyboardOpen ->
|
||||||
|
if (keyboardOpen) {
|
||||||
|
// Клавиатура открыта - emoji иконка
|
||||||
Icon(
|
Icon(
|
||||||
TablerIcons.MoodSmile,
|
TablerIcons.MoodSmile,
|
||||||
contentDescription = "Emoji",
|
contentDescription = "Emoji",
|
||||||
tint = Color.White.copy(alpha = 0.7f),
|
tint = Color.White.copy(alpha = 0.7f),
|
||||||
modifier = Modifier.size(26.dp)
|
modifier = Modifier.size(26.dp)
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// Клавиатура закрыта - камера иконка
|
||||||
|
Icon(
|
||||||
|
TablerIcons.Camera,
|
||||||
|
contentDescription = "Camera",
|
||||||
|
tint = Color.White.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.size(26.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Caption text field - простой без фона
|
// Caption text field
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = caption,
|
value = caption,
|
||||||
onValueChange = onCaptionChange,
|
onValueChange = onCaptionChange,
|
||||||
@@ -707,7 +755,8 @@ private fun TelegramCaptionBar(
|
|||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
),
|
),
|
||||||
maxLines = 4,
|
maxLines = if (isKeyboardVisible) 4 else 1,
|
||||||
|
singleLine = !isKeyboardVisible,
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
Box {
|
Box {
|
||||||
if (caption.isEmpty()) {
|
if (caption.isEmpty()) {
|
||||||
@@ -722,10 +771,17 @@ private fun TelegramCaptionBar(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send button - голубой кружок с галочкой (как в Telegram)
|
// Кнопка отправки
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = isKeyboardVisible,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(tween(150)) togetherWith fadeOut(tween(150))
|
||||||
|
},
|
||||||
|
label = "send_button"
|
||||||
|
) { keyboardOpen ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(32.dp)
|
.size(if (keyboardOpen) 32.dp else 36.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(PrimaryBlue)
|
.background(PrimaryBlue)
|
||||||
.clickable(enabled = !isSaving) { onSend() },
|
.clickable(enabled = !isSaving) { onSend() },
|
||||||
@@ -733,21 +789,25 @@ private fun TelegramCaptionBar(
|
|||||||
) {
|
) {
|
||||||
if (isSaving) {
|
if (isSaving) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(if (keyboardOpen) 18.dp else 20.dp),
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
strokeWidth = 2.dp
|
strokeWidth = 2.dp
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
// Клавиатура открыта - галочка, закрыта - стрелка отправки
|
||||||
Icon(
|
Icon(
|
||||||
TablerIcons.Check,
|
if (keyboardOpen) TablerIcons.Check else TablerIcons.Send,
|
||||||
contentDescription = "Send",
|
contentDescription = "Send",
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier
|
||||||
|
.size(if (keyboardOpen) 20.dp else 22.dp)
|
||||||
|
.then(if (!keyboardOpen) Modifier.offset(x = 1.dp) else Modifier)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Save edited image and return the URI - crops black bars from FIT_CENTER */
|
/** Save edited image and return the URI - crops black bars from FIT_CENTER */
|
||||||
private suspend fun saveEditedImage(
|
private suspend fun saveEditedImage(
|
||||||
|
|||||||
@@ -76,11 +76,13 @@ fun MediaPickerBottomSheet(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
onMediaSelected: (List<MediaItem>) -> Unit,
|
onMediaSelected: (List<MediaItem>) -> Unit,
|
||||||
|
onMediaSelectedWithCaption: ((MediaItem, String) -> Unit)? = null, // Для отправки с caption
|
||||||
onOpenCamera: () -> Unit = {},
|
onOpenCamera: () -> Unit = {},
|
||||||
onOpenFilePicker: () -> Unit = {},
|
onOpenFilePicker: () -> Unit = {},
|
||||||
onAvatarClick: () -> Unit = {},
|
onAvatarClick: () -> Unit = {},
|
||||||
currentUserPublicKey: String = "",
|
currentUserPublicKey: String = "",
|
||||||
maxSelection: Int = 10
|
maxSelection: Int = 10,
|
||||||
|
recipientName: String? = null // Имя получателя для отображения в редакторе
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -290,20 +292,14 @@ fun MediaPickerBottomSheet(
|
|||||||
onOpenCamera()
|
onOpenCamera()
|
||||||
},
|
},
|
||||||
onItemClick = { item ->
|
onItemClick = { item ->
|
||||||
// Telegram-style:
|
// Telegram-style: клик на фото сразу открывает редактор с caption
|
||||||
// - Первый клик по невыбранной фото → выбрать
|
|
||||||
// - Клик по уже выбранной фото → открыть редактор
|
|
||||||
if (item.id in selectedItems) {
|
|
||||||
// Уже выбрана - открываем редактор (только для изображений)
|
|
||||||
if (!item.isVideo) {
|
if (!item.isVideo) {
|
||||||
editingItem = item
|
editingItem = item
|
||||||
} else {
|
} else {
|
||||||
// Для видео - снимаем выделение
|
// Для видео - добавляем/убираем из selection
|
||||||
|
if (item.id in selectedItems) {
|
||||||
selectedItems = selectedItems - item.id
|
selectedItems = selectedItems - item.id
|
||||||
}
|
} else if (selectedItems.size < maxSelection) {
|
||||||
} else {
|
|
||||||
// Не выбрана - добавляем в selection
|
|
||||||
if (selectedItems.size < maxSelection) {
|
|
||||||
selectedItems = selectedItems + item.id
|
selectedItems = selectedItems + item.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,10 +330,35 @@ fun MediaPickerBottomSheet(
|
|||||||
onDismiss = { editingItem = null },
|
onDismiss = { editingItem = null },
|
||||||
onSave = { editedUri ->
|
onSave = { editedUri ->
|
||||||
editingItem = null
|
editingItem = null
|
||||||
// После редактирования открываем предпросмотр с caption
|
// Если нет onMediaSelectedWithCaption - открываем preview
|
||||||
|
if (onMediaSelectedWithCaption == null) {
|
||||||
previewPhotoUri = editedUri
|
previewPhotoUri = editedUri
|
||||||
|
} else {
|
||||||
|
// Отправляем без caption (если нажали Done вместо Send)
|
||||||
|
val mediaItem = MediaItem(
|
||||||
|
id = System.currentTimeMillis(),
|
||||||
|
uri = editedUri,
|
||||||
|
mimeType = "image/png",
|
||||||
|
dateModified = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
onMediaSelected(listOf(mediaItem))
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isDarkTheme = isDarkTheme
|
onSaveWithCaption = if (onMediaSelectedWithCaption != null) { editedUri, caption ->
|
||||||
|
editingItem = null
|
||||||
|
val mediaItem = MediaItem(
|
||||||
|
id = System.currentTimeMillis(),
|
||||||
|
uri = editedUri,
|
||||||
|
mimeType = "image/png",
|
||||||
|
dateModified = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
onMediaSelectedWithCaption(mediaItem, caption)
|
||||||
|
onDismiss()
|
||||||
|
} else null,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
showCaptionInput = onMediaSelectedWithCaption != null,
|
||||||
|
recipientName = recipientName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,10 +371,35 @@ fun MediaPickerBottomSheet(
|
|||||||
},
|
},
|
||||||
onSave = { editedUri ->
|
onSave = { editedUri ->
|
||||||
pendingPhotoUri = null
|
pendingPhotoUri = null
|
||||||
// После редактирования открываем предпросмотр с caption
|
// Если нет onMediaSelectedWithCaption - открываем preview
|
||||||
|
if (onMediaSelectedWithCaption == null) {
|
||||||
previewPhotoUri = editedUri
|
previewPhotoUri = editedUri
|
||||||
|
} else {
|
||||||
|
// Отправляем без caption
|
||||||
|
val mediaItem = MediaItem(
|
||||||
|
id = System.currentTimeMillis(),
|
||||||
|
uri = editedUri,
|
||||||
|
mimeType = "image/png",
|
||||||
|
dateModified = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
onMediaSelected(listOf(mediaItem))
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isDarkTheme = isDarkTheme
|
onSaveWithCaption = if (onMediaSelectedWithCaption != null) { editedUri, caption ->
|
||||||
|
pendingPhotoUri = null
|
||||||
|
val mediaItem = MediaItem(
|
||||||
|
id = System.currentTimeMillis(),
|
||||||
|
uri = editedUri,
|
||||||
|
mimeType = "image/png",
|
||||||
|
dateModified = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
onMediaSelectedWithCaption(mediaItem, caption)
|
||||||
|
onDismiss()
|
||||||
|
} else null,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
showCaptionInput = onMediaSelectedWithCaption != null,
|
||||||
|
recipientName = recipientName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user