From 91117986e43c691c005b0069918dfb4b05db036a Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 8 Sep 2026 22:38:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=94=B9=E4=B8=BA=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E9=80=89=E6=8B=A9=E6=A8=A1=E5=BC=8F=EF=BC=8C=E5=85=A8?= =?UTF-8?q?=E9=80=89=E4=BB=85=E4=BD=9C=E7=94=A8=E4=BA=8E=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=B0=94=E6=B3=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 SelectionContainer 包住气泡/旁白/思维链/工具卡的正文,长按可拖选;移除长按整段复制的实现与其工具文件。 --- .../top/yeij/cyrene/ui/chat/Clipboard.kt | 32 ------------------- .../top/yeij/cyrene/ui/chat/MessageBubble.kt | 32 +++++++++++-------- .../yeij/cyrene/ui/chat/StreamingBlocks.kt | 18 ++++++----- 3 files changed, 29 insertions(+), 53 deletions(-) delete mode 100644 app/src/main/kotlin/top/yeij/cyrene/ui/chat/Clipboard.kt diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/Clipboard.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/Clipboard.kt deleted file mode 100644 index 9afd758..0000000 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/Clipboard.kt +++ /dev/null @@ -1,32 +0,0 @@ -package top.yeij.cyrene.ui.chat - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.widget.Toast -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext - -/** - * 长按复制文本到系统剪贴板,并给一条 Toast 反馈。 - * 用框架的 ClipboardManager 而不是 Compose 的 LocalClipboardManager(后者在新版本已弃用)。 - */ -@Composable -fun Modifier.copyOnLongPress(text: String): Modifier { - if (text.isBlank()) return this - val context = LocalContext.current - return this.pointerInput(text) { - detectTapGestures( - onLongPress = { copyToClipboard(context, text) }, - ) - } -} - -private fun copyToClipboard(context: Context, text: String) { - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setPrimaryClip(ClipData.newPlainText("Cyrene", text)) - Toast.makeText(context, "已复制", Toast.LENGTH_SHORT).show() -} diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/MessageBubble.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/MessageBubble.kt index 0ab8a9c..c84976a 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/MessageBubble.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/MessageBubble.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -34,6 +35,9 @@ private const val CURSOR = "▍" * - 其它 mode 原样整条渲染,不解析标签; * - `thinking` 非空时(来自 REST `/messages`,owner 可见)在气泡上方显示思维链卡片; * - `image` 非空时在气泡上方显示图片。 + * + * 文本可自由选择:每个气泡/旁白各自包一层 [SelectionContainer], + * 因此选择菜单的「全选」只作用于当前那一块,而不是整个页面。 */ @Composable fun MessageBubble(message: HistoryMessageDto) { @@ -129,13 +133,14 @@ private fun Bubble( private fun AnnotationLine(parts: List) { val text = parts.joinToString(" ") { it.text }.trim() if (text.isEmpty()) return - Text( - text = text, - style = MiuixTheme.textStyles.footnote1, - fontStyle = FontStyle.Italic, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), - ) + SelectionContainer(modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)) { + Text( + text = text, + style = MiuixTheme.textStyles.footnote1, + fontStyle = FontStyle.Italic, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + } } @Composable @@ -143,7 +148,6 @@ private fun BubbleBody(text: String, isUser: Boolean) { Box( modifier = Modifier .widthIn(max = 300.dp) - .copyOnLongPress(text) .background( color = if (isUser) { MiuixTheme.colorScheme.primary @@ -154,11 +158,13 @@ private fun BubbleBody(text: String, isUser: Boolean) { ) .padding(horizontal = 14.dp, vertical = 10.dp), ) { - Text( - text = text, - style = MiuixTheme.textStyles.main, - color = if (isUser) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurfaceContainer, - ) + SelectionContainer { + Text( + text = text, + style = MiuixTheme.textStyles.main, + color = if (isUser) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurfaceContainer, + ) + } } } diff --git a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/StreamingBlocks.kt b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/StreamingBlocks.kt index c7cafff..dbe0e48 100644 --- a/app/src/main/kotlin/top/yeij/cyrene/ui/chat/StreamingBlocks.kt +++ b/app/src/main/kotlin/top/yeij/cyrene/ui/chat/StreamingBlocks.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -24,8 +25,8 @@ import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.theme.MiuixTheme /** - * 思维链卡片。默认展开(方便观察她的思考过程),可折叠。 - * 流式期间标题显示「思考中…」。 + * 思维链卡片。流式期间展开(方便观察),流式结束自动折叠;历史思维链默认折叠。 + * 正文包在 [SelectionContainer] 里,长按可自由选择、菜单「全选」只作用于本卡片。 */ @Composable fun ThinkingCard( @@ -34,7 +35,6 @@ fun ThinkingCard( modifier: Modifier = Modifier, ) { if (text.isBlank() && !active) return - // 流式期间展开(方便观察),流式结束自动折叠;历史思维链默认折叠。 var expanded by remember { mutableStateOf(active) } LaunchedEffect(active) { if (!active) expanded = false @@ -126,9 +126,11 @@ private fun FoldCardLabel(text: String) { @Composable private fun FoldCardBody(text: String) { - Text( - text = text, - style = MiuixTheme.textStyles.footnote1, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, - ) + SelectionContainer { + Text( + text = text, + style = MiuixTheme.textStyles.footnote1, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + } }