feat(chat): 改为文本选择模式,全选仅作用于当前气泡

用 SelectionContainer 包住气泡/旁白/思维链/工具卡的正文,长按可拖选;移除长按整段复制的实现与其工具文件。
This commit is contained in:
AskaEth committed 2026-09-08 22:38:28 +08:00
1 parent 33770d87a6
commit 91117986e4
3 files changed
+29 -53

No files matched your search

@@ -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()
}
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -34,6 +35,9 @@ private const val CURSOR = "▍"
* - 其它 mode 原样整条渲染,不解析标签; * - 其它 mode 原样整条渲染,不解析标签;
* - `thinking` 非空时(来自 REST `/messages`,owner 可见)在气泡上方显示思维链卡片; * - `thinking` 非空时(来自 REST `/messages`,owner 可见)在气泡上方显示思维链卡片;
* - `image` 非空时在气泡上方显示图片。 * - `image` 非空时在气泡上方显示图片。
*
* 文本可自由选择:每个气泡/旁白各自包一层 [SelectionContainer],
* 因此选择菜单的「全选」只作用于当前那一块,而不是整个页面。
*/ */
@Composable @Composable
fun MessageBubble(message: HistoryMessageDto) { fun MessageBubble(message: HistoryMessageDto) {
@@ -129,13 +133,14 @@ private fun Bubble(
private fun AnnotationLine(parts: List<ContentPart>) { private fun AnnotationLine(parts: List<ContentPart>) {
val text = parts.joinToString(" ") { it.text }.trim() val text = parts.joinToString(" ") { it.text }.trim()
if (text.isEmpty()) return if (text.isEmpty()) return
Text( SelectionContainer(modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
text = text, Text(
style = MiuixTheme.textStyles.footnote1, text = text,
fontStyle = FontStyle.Italic, style = MiuixTheme.textStyles.footnote1,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary, fontStyle = FontStyle.Italic,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
) )
}
} }
@Composable @Composable
@@ -143,7 +148,6 @@ private fun BubbleBody(text: String, isUser: Boolean) {
Box( Box(
modifier = Modifier modifier = Modifier
.widthIn(max = 300.dp) .widthIn(max = 300.dp)
.copyOnLongPress(text)
.background( .background(
color = if (isUser) { color = if (isUser) {
MiuixTheme.colorScheme.primary MiuixTheme.colorScheme.primary
@@ -154,11 +158,13 @@ private fun BubbleBody(text: String, isUser: Boolean) {
) )
.padding(horizontal = 14.dp, vertical = 10.dp), .padding(horizontal = 14.dp, vertical = 10.dp),
) { ) {
Text( SelectionContainer {
text = text, Text(
style = MiuixTheme.textStyles.main, text = text,
color = if (isUser) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurfaceContainer, style = MiuixTheme.textStyles.main,
) color = if (isUser) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurfaceContainer,
)
}
} }
} }
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -24,8 +25,8 @@ import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
/** /**
* 思维链卡片。默认展开(方便观察她的思考过程),可折叠。 * 思维链卡片。流式期间展开(方便观察),流式结束自动折叠;历史思维链默认折叠。
* 流式期间标题显示「思考中…」。 * 正文包在 [SelectionContainer] 里,长按可自由选择、菜单「全选」只作用于本卡片。
*/ */
@Composable @Composable
fun ThinkingCard( fun ThinkingCard(
@@ -34,7 +35,6 @@ fun ThinkingCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (text.isBlank() && !active) return if (text.isBlank() && !active) return
// 流式期间展开(方便观察),流式结束自动折叠;历史思维链默认折叠。
var expanded by remember { mutableStateOf(active) } var expanded by remember { mutableStateOf(active) }
LaunchedEffect(active) { LaunchedEffect(active) {
if (!active) expanded = false if (!active) expanded = false
@@ -126,9 +126,11 @@ private fun FoldCardLabel(text: String) {
@Composable @Composable
private fun FoldCardBody(text: String) { private fun FoldCardBody(text: String) {
Text( SelectionContainer {
text = text, Text(
style = MiuixTheme.textStyles.footnote1, text = text,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary, style = MiuixTheme.textStyles.footnote1,
) color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
}
} }