diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 6ae74c7..ea730fe 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -72,6 +72,7 @@ import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.data.ForwardManager import com.rosetta.messenger.ui.chats.ForwardChatPickerBottomSheet import com.rosetta.messenger.utils.MediaUtils +import com.rosetta.messenger.ui.chats.components.ImageEditorScreen import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator import androidx.compose.runtime.collectAsState import androidx.activity.compose.rememberLauncherForActivityResult @@ -181,23 +182,20 @@ fun ChatDetailScreen( var showImageViewer by remember { mutableStateOf(false) } var imageViewerInitialIndex by remember { mutableStateOf(0) } - // �📷 Camera: URI для сохранения фото + // 📷 Camera: URI для сохранения фото var cameraImageUri by remember { mutableStateOf(null) } + // 📷 Состояние для flow камеры: фото → редактор с caption → отправка + var pendingCameraPhotoUri by remember { mutableStateOf(null) } // Фото для редактирования + // 📷 Camera launcher val cameraLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.TakePicture() ) { success -> if (success && cameraImageUri != null) { - scope.launch { - android.util.Log.d("ChatDetailScreen", "📷 Photo captured: $cameraImageUri") - // Конвертируем и отправляем - val base64 = MediaUtils.uriToBase64Image(context, cameraImageUri!!) - val blurhash = MediaUtils.generateBlurhash(context, cameraImageUri!!) - if (base64 != null) { - viewModel.sendImageMessage(base64, blurhash) - } - } + android.util.Log.d("ChatDetailScreen", "📷 Photo captured: $cameraImageUri") + // Открываем редактор вместо прямой отправки + pendingCameraPhotoUri = cameraImageUri } } @@ -2004,8 +2002,9 @@ fun ChatDetailScreen( // Изображение val base64 = MediaUtils.uriToBase64Image(context, item.uri) val blurhash = MediaUtils.generateBlurhash(context, item.uri) + val (width, height) = MediaUtils.getImageDimensions(context, item.uri) if (base64 != null) { - imageDataList.add(ChatViewModel.ImageData(base64, blurhash)) + imageDataList.add(ChatViewModel.ImageData(base64, blurhash, width, height)) } } } @@ -2044,4 +2043,39 @@ fun ChatDetailScreen( viewModel.sendAvatarMessage() } ) + + // 📷 Image Editor для фото с камеры (с caption как в Telegram) + pendingCameraPhotoUri?.let { uri -> + ImageEditorScreen( + imageUri = uri, + onDismiss = { + pendingCameraPhotoUri = null + }, + onSave = { editedUri -> + // Fallback если onSaveWithCaption не сработал + pendingCameraPhotoUri = null + scope.launch { + val base64 = MediaUtils.uriToBase64Image(context, editedUri) + val blurhash = MediaUtils.generateBlurhash(context, editedUri) + val (width, height) = MediaUtils.getImageDimensions(context, editedUri) + if (base64 != null) { + viewModel.sendImageMessage(base64, blurhash, "", width, height) + } + } + }, + onSaveWithCaption = { editedUri, caption -> + pendingCameraPhotoUri = null + scope.launch { + val base64 = MediaUtils.uriToBase64Image(context, editedUri) + val blurhash = MediaUtils.generateBlurhash(context, editedUri) + val (width, height) = MediaUtils.getImageDimensions(context, editedUri) + if (base64 != null) { + viewModel.sendImageMessage(base64, blurhash, caption, width, height) + } + } + }, + isDarkTheme = isDarkTheme, + showCaptionInput = true + ) + } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index bd13450..7d3c9d4 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -1418,8 +1418,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { * @param imageBase64 Base64 изображения * @param blurhash Blurhash preview изображения * @param caption Подпись к изображению (опционально) + * @param width Ширина изображения (для правильного отображения) + * @param height Высота изображения (для правильного отображения) */ - fun sendImageMessage(imageBase64: String, blurhash: String, caption: String = "") { + fun sendImageMessage(imageBase64: String, blurhash: String, caption: String = "", width: Int = 0, height: Int = 0) { val recipient = opponentKey val sender = myPublicKey val privateKey = myPrivateKey @@ -1451,7 +1453,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { id = "img_$timestamp", type = AttachmentType.IMAGE, preview = blurhash, - blob = imageBase64 // Для локального отображения + blob = imageBase64, // Для локального отображения + width = width, + height = height ) ) ) @@ -1493,7 +1497,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { id = attachmentId, blob = "", // 🔥 Пустой blob - файл на Transport Server! type = AttachmentType.IMAGE, - preview = previewWithTag + preview = previewWithTag, + width = width, + height = height ) val packet = PacketMessage().apply { @@ -1533,6 +1539,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { put("type", AttachmentType.IMAGE.value) put("preview", previewWithTag) put("blob", "") // Пустой blob - не сохраняем в БД! + put("width", width) + put("height", height) }) }.toString() @@ -1566,14 +1574,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { * @param images Список пар (base64, blurhash) для каждого изображения * @param caption Подпись к группе изображений (опционально) */ - data class ImageData(val base64: String, val blurhash: String) + data class ImageData(val base64: String, val blurhash: String, val width: Int = 0, val height: Int = 0) fun sendImageGroup(images: List, caption: String = "") { if (images.isEmpty()) return // Если одно изображение - отправляем обычным способом if (images.size == 1) { - sendImageMessage(images[0].base64, images[0].blurhash, caption) + sendImageMessage(images[0].base64, images[0].blurhash, caption, images[0].width, images[0].height) return } @@ -1602,7 +1610,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { id = "img_${timestamp}_$index", type = AttachmentType.IMAGE, preview = imageData.blurhash, - blob = imageData.base64 // Для локального отображения + blob = imageData.base64, // Для локального отображения + width = imageData.width, + height = imageData.height ) } @@ -1662,7 +1672,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { id = attachmentId, blob = if (uploadTag != null) "" else encryptedImageBlob, type = AttachmentType.IMAGE, - preview = previewWithTag + preview = previewWithTag, + width = imageData.width, + height = imageData.height )) // Для БД @@ -1671,6 +1683,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { put("type", AttachmentType.IMAGE.value) put("preview", previewWithTag) put("blob", "") // Пустой blob - изображения в файловой системе + put("width", imageData.width) + put("height", imageData.height) }) Log.d(TAG, "🖼️ Image $index uploaded: tag=${uploadTag?.take(20) ?: "null"}") diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt index b52b125..d1732a7 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt @@ -649,41 +649,58 @@ fun ImageAttachment( } // 📐 Вычисляем размер пузырька на основе соотношения сторон изображения (как в Telegram) - val (imageWidth, imageHeight) = remember(attachment.width, attachment.height, fillMaxSize, aspectRatio) { + // Используем размеры из attachment или из загруженного bitmap + val actualWidth = if (attachment.width > 0) attachment.width else imageBitmap?.width ?: 0 + val actualHeight = if (attachment.height > 0) attachment.height else imageBitmap?.height ?: 0 + + val (imageWidth, imageHeight) = remember(actualWidth, actualHeight, fillMaxSize, aspectRatio) { // Если fillMaxSize - используем 100% контейнера if (fillMaxSize) { null to null } else { - val maxWidth = 260.dp - val maxHeight = 340.dp - val minWidth = 180.dp - val minHeight = 140.dp + // Telegram-style размеры - больше и адаптивнее + val maxWidth = 280.dp + val maxHeight = 400.dp // Увеличено для вертикальных фото + val minWidth = 160.dp + val minHeight = 120.dp - if (attachment.width > 0 && attachment.height > 0) { - val ar = attachment.width.toFloat() / attachment.height.toFloat() + if (actualWidth > 0 && actualHeight > 0) { + val ar = actualWidth.toFloat() / actualHeight.toFloat() when { + // Очень широкое изображение (panorama) + ar > 2.0f -> { + val width = maxWidth + val height = (maxWidth.value / ar).dp.coerceIn(minHeight, maxHeight) + width to height + } // Широкое изображение (landscape) ar > 1.2f -> { val width = maxWidth val height = (maxWidth.value / ar).dp.coerceIn(minHeight, maxHeight) width to height } + // Очень высокое изображение (story-like) + ar < 0.5f -> { + val height = maxHeight + val width = (maxHeight.value * ar).dp.coerceIn(minWidth, maxWidth) + width to height + } // Высокое изображение (portrait) - ar < 0.8f -> { + ar < 0.85f -> { val height = maxHeight val width = (maxHeight.value * ar).dp.coerceIn(minWidth, maxWidth) width to height } // Квадратное или близкое к квадрату else -> { - val size = 220.dp + val size = 240.dp size to size } } } else { - // Fallback если размеры не указаны - 220.dp to 220.dp + // Fallback если размеры не указаны - квадрат средний + 240.dp to 240.dp } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index d98eed1..bad6c1b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -331,8 +331,19 @@ fun MessageBubble( message.replyData == null && message.attachments.all { it.type == com.rosetta.messenger.network.AttachmentType.IMAGE } + // Фото + caption (как в Telegram) + val hasImageWithCaption = message.attachments.isNotEmpty() && + message.text.isNotEmpty() && + message.replyData == null && + message.attachments.all { it.type == com.rosetta.messenger.network.AttachmentType.IMAGE } + // Для сообщений только с фото - минимальный padding и тонкий border - val bubblePadding = if (hasOnlyMedia) PaddingValues(0.dp) else PaddingValues(horizontal = 10.dp, vertical = 8.dp) + // Для фото + caption - padding только внизу для текста + val bubblePadding = when { + hasOnlyMedia -> PaddingValues(0.dp) + hasImageWithCaption -> PaddingValues(0.dp) + else -> PaddingValues(horizontal = 10.dp, vertical = 8.dp) + } val bubbleBorderWidth = if (hasOnlyMedia) 1.dp else 0.dp Box( @@ -395,9 +406,56 @@ fun MessageBubble( currentUserPublicKey = currentUserPublicKey, onImageClick = onImageClick ) - if (message.text.isNotEmpty()) { - Spacer(modifier = Modifier.height(8.dp)) + } + + // 🖼️ Caption под фото (как в Telegram) + if (hasImageWithCaption) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(bubbleColor) + .padding(horizontal = 10.dp, vertical = 8.dp) + ) { + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.fillMaxWidth() + ) { + AppleEmojiText( + text = message.text, + color = textColor, + fontSize = 16.sp, + modifier = Modifier.weight(1f, fill = false) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.padding(bottom = 2.dp) + ) { + Text( + text = timeFormat.format(message.timestamp), + color = timeColor, + fontSize = 11.sp, + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic + ) + if (message.isOutgoing) { + val displayStatus = if (isSavedMessages) MessageStatus.READ else message.status + AnimatedMessageStatus( + status = displayStatus, + timeColor = timeColor, + timestamp = message.timestamp.time, + onRetry = onRetry, + onDelete = onDelete + ) + } + } + } } + } else if (message.attachments.isNotEmpty() && message.text.isNotEmpty()) { + // Обычное фото + текст (не только изображения) + Spacer(modifier = Modifier.height(8.dp)) } // Если есть reply - текст слева, время справа на одной строке @@ -438,8 +496,8 @@ fun MessageBubble( } } } - } else if (!hasOnlyMedia && message.text.isNotEmpty()) { - // Без reply и не только фото - компактно в одну строку + } else if (!hasOnlyMedia && !hasImageWithCaption && message.text.isNotEmpty()) { + // Без reply, не только фото, и не фото с caption - компактно в одну строку Row( verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(10.dp), diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt index d549086..d827b88 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt @@ -75,7 +75,7 @@ val drawingColors = listOf( ) /** - * Telegram-style image editor screen + * Telegram-style image editor screen with caption input */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -83,7 +83,9 @@ fun ImageEditorScreen( imageUri: Uri, onDismiss: () -> Unit, onSave: (Uri) -> Unit, - isDarkTheme: Boolean = true + onSaveWithCaption: ((Uri, String) -> Unit)? = null, // New callback with caption + isDarkTheme: Boolean = true, + showCaptionInput: Boolean = false // Show caption input for camera flow ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -96,6 +98,9 @@ fun ImageEditorScreen( var showBrushSizeSlider by remember { mutableStateOf(false) } var isSaving by remember { mutableStateOf(false) } + // Caption state + var caption by remember { mutableStateOf("") } + // Current image URI (can change after crop) var currentImageUri by remember { mutableStateOf(imageUri) } @@ -170,33 +175,35 @@ fun ImageEditorScreen( ) } - // Done/Save button - TextButton( - onClick = { - scope.launch { - isSaving = true - saveEditedImage(context, photoEditor) { savedUri -> - isSaving = false - if (savedUri != null) { - onSave(savedUri) + // Done/Save button (only show if no caption input) + if (!showCaptionInput) { + TextButton( + onClick = { + scope.launch { + isSaving = true + saveEditedImage(context, photoEditor) { savedUri -> + isSaving = false + if (savedUri != null) { + onSave(savedUri) + } } } + }, + enabled = !isSaving + ) { + if (isSaving) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = PrimaryBlue, + strokeWidth = 2.dp + ) + } else { + Text( + "Done", + color = PrimaryBlue, + fontWeight = FontWeight.SemiBold + ) } - }, - enabled = !isSaving - ) { - if (isSaving) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = PrimaryBlue, - strokeWidth = 2.dp - ) - } else { - Text( - "Done", - color = PrimaryBlue, - fontWeight = FontWeight.SemiBold - ) } } }, @@ -282,6 +289,30 @@ fun ImageEditorScreen( ) } + // Caption input bar (for camera flow like Telegram) - ВЫШЕ иконок + if (showCaptionInput) { + CaptionInputBar( + caption = caption, + onCaptionChange = { caption = it }, + isSaving = isSaving, + onSend = { + scope.launch { + isSaving = true + saveEditedImage(context, photoEditor) { savedUri -> + isSaving = false + if (savedUri != null) { + if (onSaveWithCaption != null) { + onSaveWithCaption(savedUri, caption) + } else { + onSave(savedUri) + } + } + } + } + } + ) + } + // Bottom toolbar with tools BottomToolbar( currentTool = currentTool, @@ -342,6 +373,77 @@ fun ImageEditorScreen( } } +/** + * Caption input bar with send button (like Telegram) + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CaptionInputBar( + caption: String, + onCaptionChange: (String) -> Unit, + isSaving: Boolean, + onSend: () -> Unit +) { + Surface( + color = Color(0xFF1C1C1E), + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .imePadding() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Caption text field + OutlinedTextField( + value = caption, + onValueChange = onCaptionChange, + modifier = Modifier.weight(1f), + placeholder = { + Text( + "Add a caption...", + color = Color.White.copy(alpha = 0.5f) + ) + }, + maxLines = 3, + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + cursorColor = PrimaryBlue, + focusedBorderColor = PrimaryBlue, + unfocusedBorderColor = Color.White.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(20.dp) + ) + + // Send button + FloatingActionButton( + onClick = onSend, + containerColor = PrimaryBlue, + modifier = Modifier.size(48.dp), + elevation = FloatingActionButtonDefaults.elevation(0.dp) + ) { + if (isSaving) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Icon( + imageVector = TablerIcons.Send, + contentDescription = "Send", + tint = Color.White, + modifier = Modifier.size(22.dp) + ) + } + } + } + } +} + @Composable private fun BottomToolbar( currentTool: EditorTool, diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt index df7ffd1..d7bcc3e 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt @@ -96,6 +96,15 @@ fun MediaPickerBottomSheet( // Editor state - when user taps on a photo, open editor var editingItem by remember { mutableStateOf(null) } + // Uri фото, только что сделанного с камеры (для редактирования) + var pendingPhotoUri by remember { mutableStateOf(null) } + + // Uri отредактированного фото (для предпросмотра с caption) + var previewPhotoUri by remember { mutableStateOf(null) } + + // Caption для фото + var photoCaption by remember { mutableStateOf("") } + // Permission launcher val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions() @@ -148,8 +157,8 @@ fun MediaPickerBottomSheet( val textColor = if (isDarkTheme) Color.White else Color.Black val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93) - // Show gallery only if not editing - if (isVisible && editingItem == null) { + // Show gallery only if not editing and not in preview + if (isVisible && editingItem == null && pendingPhotoUri == null && previewPhotoUri == null) { ModalBottomSheet( onDismissRequest = onDismiss, containerColor = backgroundColor, @@ -318,23 +327,59 @@ fun MediaPickerBottomSheet( } } - // Image Editor overlay + // Image Editor overlay для фото из галереи editingItem?.let { item -> ImageEditorScreen( imageUri = item.uri, onDismiss = { editingItem = null }, onSave = { editedUri -> editingItem = null - // Create a new MediaItem with the edited URI - val editedItem = MediaItem( + // После редактирования открываем предпросмотр с caption + previewPhotoUri = editedUri + }, + isDarkTheme = isDarkTheme + ) + } + + // Image Editor overlay для фото с камеры + pendingPhotoUri?.let { uri -> + ImageEditorScreen( + imageUri = uri, + onDismiss = { + pendingPhotoUri = null + }, + onSave = { editedUri -> + pendingPhotoUri = null + // После редактирования открываем предпросмотр с caption + previewPhotoUri = editedUri + }, + isDarkTheme = isDarkTheme + ) + } + + // Preview with caption before sending (как в Telegram) + previewPhotoUri?.let { uri -> + PhotoPreviewWithCaptionScreen( + imageUri = uri, + caption = photoCaption, + onCaptionChange = { photoCaption = it }, + onSend = { + val item = MediaItem( id = System.currentTimeMillis(), - uri = editedUri, + uri = uri, mimeType = "image/png", dateModified = System.currentTimeMillis() ) - onMediaSelected(listOf(editedItem)) + // Отправляем фото (caption можно передать отдельно если нужно) + onMediaSelected(listOf(item)) + previewPhotoUri = null + photoCaption = "" onDismiss() }, + onDismiss = { + previewPhotoUri = null + photoCaption = "" + }, isDarkTheme = isDarkTheme ) } @@ -890,3 +935,113 @@ private fun formatDuration(durationMs: Long): String { val seconds = totalSeconds % 60 return "%d:%02d".format(minutes, seconds) } + +/** + * Экран предпросмотра фото с caption и кнопкой отправки (как в Telegram) + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PhotoPreviewWithCaptionScreen( + imageUri: Uri, + caption: String, + onCaptionChange: (String) -> Unit, + onSend: () -> Unit, + onDismiss: () -> Unit, + isDarkTheme: Boolean +) { + val backgroundColor = if (isDarkTheme) Color.Black else Color.White + val textColor = if (isDarkTheme) Color.White else Color.Black + + Surface( + color = backgroundColor, + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding() + ) { + // Top bar with close button + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = onDismiss) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + tint = textColor + ) + } + } + + // Image preview + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUri) + .crossfade(true) + .build(), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit + ) + } + + // Caption input + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = caption, + onValueChange = onCaptionChange, + modifier = Modifier.weight(1f), + placeholder = { + Text( + "Add a caption...", + color = textColor.copy(alpha = 0.5f) + ) + }, + maxLines = 3, + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedTextColor = textColor, + unfocusedTextColor = textColor, + cursorColor = PrimaryBlue, + focusedBorderColor = PrimaryBlue, + unfocusedBorderColor = textColor.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(20.dp) + ) + + // Send button + FloatingActionButton( + onClick = onSend, + containerColor = PrimaryBlue, + modifier = Modifier.size(56.dp) + ) { + Icon( + imageVector = TablerIcons.Send, + contentDescription = "Send", + tint = Color.White, + modifier = Modifier.size(24.dp) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + } + } +} diff --git a/app/src/main/java/com/rosetta/messenger/utils/MediaUtils.kt b/app/src/main/java/com/rosetta/messenger/utils/MediaUtils.kt index a08bcc0..91f48b6 100644 --- a/app/src/main/java/com/rosetta/messenger/utils/MediaUtils.kt +++ b/app/src/main/java/com/rosetta/messenger/utils/MediaUtils.kt @@ -216,4 +216,65 @@ object MediaUtils { "" } } + + /** + * Получить размеры изображения из Uri без полной загрузки в память + */ + fun getImageDimensions(context: Context, uri: Uri): Pair { + return try { + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true // Не загружаем в память, только размеры + } + context.contentResolver.openInputStream(uri)?.use { inputStream -> + BitmapFactory.decodeStream(inputStream, null, options) + } + + var width = options.outWidth + var height = options.outHeight + + // Учитываем масштабирование (как в uriToBase64Image) + if (width > MAX_IMAGE_SIZE || height > MAX_IMAGE_SIZE) { + val ratio = width.toFloat() / height.toFloat() + if (width > height) { + width = MAX_IMAGE_SIZE + height = (MAX_IMAGE_SIZE / ratio).toInt() + } else { + height = MAX_IMAGE_SIZE + width = (MAX_IMAGE_SIZE * ratio).toInt() + } + } + + Log.d(TAG, "📐 Image dimensions: ${width}x${height}") + Pair(width, height) + } catch (e: Exception) { + Log.e(TAG, "📐 Failed to get image dimensions", e) + Pair(0, 0) + } + } + + /** + * Получить размеры изображения из Base64 строки + */ + fun getImageDimensionsFromBase64(base64: String): Pair { + return try { + // Убираем data URI prefix если есть + val base64Data = if (base64.contains(",")) { + base64.substringAfter(",") + } else { + base64 + } + + val bytes = Base64.decode(base64Data, Base64.DEFAULT) + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) + + Log.d(TAG, "📐 Image dimensions from base64: ${options.outWidth}x${options.outHeight}") + Pair(options.outWidth, options.outHeight) + } catch (e: Exception) { + Log.e(TAG, "📐 Failed to get image dimensions from base64", e) + Pair(0, 0) + } + } }