dm-69
This commit is contained in:
parent
1ecbc7b156
commit
f088fc746f
3 changed files with 135 additions and 89 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package com.example.login_v3.home.Message.UI.Detail
|
package com.example.login_v3.home.Message.UI.Detail
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
@ -307,6 +308,7 @@ fun MessageList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String?.isNullTabOrBlank(): Boolean = this == null || this.trim().isBlank()
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageRow(
|
fun MessageRow(
|
||||||
message: Message,
|
message: Message,
|
||||||
|
|
@ -324,8 +326,30 @@ fun MessageRow(
|
||||||
val isLegacyImage = contentLowerCase.startsWith("http") && imageExtensions.any { contentLowerCase.contains(it) }
|
val isLegacyImage = contentLowerCase.startsWith("http") && imageExtensions.any { contentLowerCase.contains(it) }
|
||||||
val isImage = isAttachmentImage || isLegacyImage
|
val isImage = isAttachmentImage || isLegacyImage
|
||||||
|
|
||||||
|
|
||||||
val rawContent = message.content ?: ""
|
val rawContent = message.content ?: ""
|
||||||
val finalImageModel = if (rawContent.startsWith("/")) "http://192.168.0.217$rawContent" else rawContent
|
// 💡 核心修正:利用 remember 根據 message 狀態動態計算正確的圖片 URL
|
||||||
|
val finalImageModel = remember(message) {
|
||||||
|
// 1. 如果有 attachment,優先去拿裡面的 filename;如果沒有,再拿一般的 content
|
||||||
|
// 2. 假設後端儲存路徑通常是在某個特定資料夾(例如 /uploads/),如果 filename 沒帶路徑,記得補上
|
||||||
|
val rawPath = if (isAttachmentImage && !message.attachment?.filename.isNullTabOrBlank()) {
|
||||||
|
message.attachment!!.filename
|
||||||
|
} else {
|
||||||
|
message.content
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
rawPath.isBlank() -> ""
|
||||||
|
rawPath.startsWith("https") -> rawPath // 已經是完整網址,直接回傳
|
||||||
|
|
||||||
|
// 💡 注意:請根據你後端圖片實際存放的路由修改這裡。
|
||||||
|
// 如果後端 filename 只有 "abc.jpg",而實際上網址是 "http://192.168.0.217/uploads/abc.jpg"
|
||||||
|
// 你需要把下面改成 "/uploads/$rawPath"
|
||||||
|
rawPath.startsWith("/") -> "https://192.168.0.217$rawPath"
|
||||||
|
|
||||||
|
else -> "https://192.168.0.217/$rawPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//emoji value
|
//emoji value
|
||||||
var showEmojiPanel by remember { mutableStateOf(false) }
|
var showEmojiPanel by remember { mutableStateOf(false) }
|
||||||
|
|
@ -424,7 +448,15 @@ fun MessageRow(
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 300.dp)
|
.heightIn(max = 300.dp),
|
||||||
|
// 🛠️ 移除了 unresolved 的 debugPlaceholder
|
||||||
|
onError = { errorState ->
|
||||||
|
// 這樣就能在 Logcat 裡過濾 "ChatImageError" 關鍵字,看看到底是哪個網址拼錯了!
|
||||||
|
android.util.Log.e(
|
||||||
|
"ChatImageError",
|
||||||
|
"圖片載入失敗, 網址為: $finalImageModel, 原因: ${errorState.result.throwable}"
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -32,6 +33,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -61,100 +63,112 @@ fun MessageInputBar(
|
||||||
.navigationBarsPadding() // 自動適應 Android 系統導覽列高度
|
.navigationBarsPadding() // 自動適應 Android 系統導覽列高度
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp) // 懸浮的外邊距
|
.padding(horizontal = 16.dp, vertical = 8.dp) // 懸浮的外邊距
|
||||||
) {
|
) {
|
||||||
// 使用 Surface 或 Card 來實作圓角與陰影懸浮感
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(16.dp), // 整體大圓角化
|
|
||||||
color = Color.White,
|
|
||||||
tonalElevation = 6.dp, // 頂級懸浮感
|
|
||||||
shadowElevation = 6.dp // 實體陰影
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
|
|
||||||
// 1. 回覆預覽列 (加入頂部圓角修飾)
|
//glass effect
|
||||||
AnimatedVisibility(visible = replyingMessage != null) {
|
Box(
|
||||||
if (replyingMessage != null) {
|
modifier = Modifier
|
||||||
Row(
|
.matchParentSize()
|
||||||
modifier = Modifier
|
.blur(10.dp)
|
||||||
.fillMaxWidth()
|
)
|
||||||
.background(Color(0xFFF8F9FA)) // 更輕盈的底色
|
Box(
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
modifier = Modifier
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
.matchParentSize()
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
.background(
|
||||||
) {
|
color = MaterialTheme.colorScheme.primary,
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
RoundedCornerShape(16.dp)
|
||||||
Text(
|
)
|
||||||
text = replyingMessage.content,
|
.border(
|
||||||
style = MaterialTheme.typography.bodySmall,
|
1.dp,
|
||||||
color = Color.Gray,
|
Color.White.copy(alpha = 0.3f),
|
||||||
maxLines = 1,
|
RoundedCornerShape(16.dp)
|
||||||
overflow = TextOverflow.Ellipsis
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
IconButton(onClick = onCancelReply, modifier = Modifier.size(24.dp)) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Close,
|
// 1. 回覆預覽列 (加入頂部圓角修飾)
|
||||||
contentDescription = "取消回覆",
|
AnimatedVisibility(visible = replyingMessage != null) {
|
||||||
tint = Color.Gray
|
if (replyingMessage != null) {
|
||||||
)
|
Row(
|
||||||
}
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(0xFFF8F9FA)) // 更輕盈的底色
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = replyingMessage.content,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onCancelReply, modifier = Modifier.size(24.dp)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "取消回覆",
|
||||||
|
tint = Color.Gray
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 實際的輸入工具列
|
// 2. 實際的輸入工具列
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 圖片選擇按鈕
|
// 圖片選擇按鈕
|
||||||
IconButton(onClick = { imagePickerLauncher.launch("image/*") }) {
|
IconButton(onClick = { imagePickerLauncher.launch("image/*") }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Photo,
|
imageVector = Icons.Default.Photo,
|
||||||
contentDescription = "選擇圖片",
|
contentDescription = "選擇圖片",
|
||||||
tint = Color.Gray
|
tint = Color.Gray
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 輸入文字框 (改為內嵌樣式,去掉自帶的邊框,讓整體更有一體感)
|
|
||||||
BasicTextField(
|
|
||||||
value = textState,
|
|
||||||
onValueChange = { textState = it },
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
textStyle = MaterialTheme.typography.bodyLarge,
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
if (textState.isEmpty()) {
|
|
||||||
Text("請輸入訊息...", color = Color.LightGray)
|
|
||||||
}
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 送出按鈕
|
// 輸入文字框 (改為內嵌樣式,去掉自帶的邊框,讓整體更有一體感)
|
||||||
IconButton(
|
BasicTextField(
|
||||||
onClick = {
|
value = textState,
|
||||||
if (textState.isNotBlank() && !isLoading) {
|
onValueChange = { textState = it },
|
||||||
onSendClick(textState)
|
modifier = Modifier
|
||||||
textState = ""
|
.weight(1f)
|
||||||
}
|
.padding(horizontal = 8.dp),
|
||||||
},
|
textStyle = MaterialTheme.typography.bodyLarge,
|
||||||
enabled = textState.isNotBlank() && !isLoading
|
decorationBox = { innerTextField ->
|
||||||
) {
|
if (textState.isEmpty()) {
|
||||||
if (isLoading) {
|
Text("請輸入訊息...", color = Color.LightGray)
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
|
||||||
} else {
|
|
||||||
// 加上小背景讓送出按鈕更有重點
|
|
||||||
val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Send,
|
|
||||||
contentDescription = "送出",
|
|
||||||
tint = iconColor
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 送出按鈕
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (textState.isNotBlank() && !isLoading) {
|
||||||
|
onSendClick(textState)
|
||||||
|
textState = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = textState.isNotBlank() && !isLoading
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||||
|
} else {
|
||||||
|
// 加上小背景讓送出按鈕更有重點
|
||||||
|
val iconColor = if (textState.isNotBlank()) Color(0xFFDA7029) else Color.LightGray
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Send,
|
||||||
|
contentDescription = "送出",
|
||||||
|
tint = iconColor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<network-security-config>
|
<network-security-config>
|
||||||
<domain-config cleartextTrafficPermitted="false">
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
<domain includeSubdomains="true">192.168.0.217</domain>
|
<domain includeSubdomains="true">192.168.0.217</domain>
|
||||||
|
|
||||||
<trust-anchors>
|
<trust-anchors>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue