From 77eef185f5c36039a97fbc1d95c1670fcc4b0f82 Mon Sep 17 00:00:00 2001 From: benya Date: Mon, 6 Apr 2026 01:51:36 +0300 Subject: [PATCH] feat: add media selection mode to chat info grid --- .../messenger/ui/chat/ChatScreen.kt | 180 ++++++++++++++---- .../app/src/main/res/values-ru/strings.xml | 4 + android/app/src/main/res/values/strings.xml | 4 + 3 files changed, 151 insertions(+), 37 deletions(-) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index 4055545..213a83c 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -5938,6 +5938,17 @@ private data class ChatInfoEntry( val previewIsVideo: Boolean = false, ) +private fun buildChatInfoEntrySelectionKey(entry: ChatInfoEntry): String { + return listOfNotNull( + entry.type.name, + entry.sourceMessageId?.toString(), + entry.resourceUrl, + entry.previewImageUrl, + entry.title, + ).joinToString("|") +} + +@OptIn(ExperimentalFoundationApi::class) @Composable private fun ChatInfoTabContent( tab: ChatInfoTab, @@ -6013,58 +6024,153 @@ private fun ChatInfoTabContent( return } if (tab == ChatInfoTab.Media) { - LazyVerticalGrid( - columns = GridCells.Fixed(3), + var selectedMediaKeys by remember(filtered) { mutableStateOf(setOf()) } + val selectionCount = selectedMediaKeys.size + val selectedEntries = remember(filtered, selectionCount) { + filtered.filter { entry -> buildChatInfoEntrySelectionKey(entry) in selectedMediaKeys } + } + Column( modifier = Modifier .fillMaxWidth() - .height(320.dp), - verticalArrangement = Arrangement.spacedBy(2.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp), + .height(372.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), ) { - items(filtered.take(120)) { entry -> - Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(4.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)) - .clickable { onEntryClick(entry) }, + AnimatedVisibility(visible = selectionCount > 0) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.72f), ) { - if (!entry.previewImageUrl.isNullOrBlank()) { - AsyncImage( - model = entry.previewImageUrl, - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - ) - } else { - Icon( - imageVector = Icons.Filled.Movie, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .align(Alignment.Center) - .size(20.dp), + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = stringResource(R.string.chat_info_selected_count, selectionCount), + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, ) + if (selectedEntries.size == 1) { + TextButton(onClick = { onEntryClick(selectedEntries.first()) }) { + Text(stringResource(id = R.string.chat_info_open_selected)) + } + } + TextButton(onClick = { selectedMediaKeys = emptySet() }) { + Text(stringResource(id = R.string.chat_info_clear_selection)) + } } - if (entry.previewIsVideo) { + } + } + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 96.dp), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + items(filtered.take(120)) { entry -> + val selectionKey = remember(entry) { buildChatInfoEntrySelectionKey(entry) } + val isSelected = selectionKey in selectedMediaKeys + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)) + .combinedClickable( + onClick = { + if (selectionCount > 0) { + if (isSelected) { + selectedMediaKeys = selectedMediaKeys - selectionKey + } else { + selectedMediaKeys = selectedMediaKeys + selectionKey + } + } else { + onEntryClick(entry) + } + }, + onLongClick = { + if (isSelected) { + selectedMediaKeys = selectedMediaKeys - selectionKey + } else { + selectedMediaKeys = selectedMediaKeys + selectionKey + } + }, + ), + ) { + if (!entry.previewImageUrl.isNullOrBlank()) { + AsyncImage( + model = entry.previewImageUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } else { + Icon( + imageVector = Icons.Filled.Movie, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .align(Alignment.Center) + .size(20.dp), + ) + } + if (entry.previewIsVideo) { + Surface( + shape = RoundedCornerShape(8.dp), + color = Color.Black.copy(alpha = 0.5f), + modifier = Modifier + .align(Alignment.BottomStart) + .padding(6.dp), + ) { + Text( + text = stringResource(id = R.string.chat_media_badge_video), + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + style = MaterialTheme.typography.labelSmall, + color = Color.White, + ) + } + } + if (isSelected) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.22f)), + ) + } Surface( - shape = RoundedCornerShape(8.dp), - color = Color.Black.copy(alpha = 0.5f), modifier = Modifier - .align(Alignment.BottomStart) + .align(Alignment.TopEnd) .padding(6.dp), + shape = CircleShape, + color = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + Color.Black.copy(alpha = 0.38f) + }, ) { - Text( - text = stringResource(id = R.string.chat_media_badge_video), - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - style = MaterialTheme.typography.labelSmall, - color = Color.White, + Icon( + imageVector = if (isSelected) Icons.Filled.Check else Icons.Filled.RadioButtonUnchecked, + contentDescription = null, + tint = Color.White, + modifier = Modifier + .padding(4.dp) + .size(14.dp), ) } } } } + Text( + text = stringResource(id = R.string.chat_info_media_selection_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } return } diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index f62a71b..884524f 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -162,6 +162,10 @@ Участники Пока нет: %1$s Нет данных об участниках + Выбрано: %1$d + Открыть + Сбросить + Удерживайте медиа, чтобы перейти в режим выбора. Участники (%1$d) Заблокированные (%1$d) Повысить diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index dcbdc7a..59ce07c 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -162,6 +162,10 @@ Members No %1$s yet No members data + %1$d selected + Open + Clear + Long press media to select multiple items. Members (%1$d) Banned (%1$d) Promote