dm-150
This commit is contained in:
parent
4733812a9c
commit
e054c014ad
2 changed files with 133 additions and 1 deletions
|
|
@ -1,2 +1,102 @@
|
||||||
package com.example.login_v3.home.Message.UI.Detail.Message_Component
|
package com.example.login_v3.home.Message.UI.Detail.Message_Component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Pause
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.login_v3.data.api.api_class.Message
|
||||||
|
import com.example.login_v3.home.Message.ViewModel.Detail.ChatViewModel
|
||||||
|
import okhttp3.internal.concurrent.formatDuration
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AudioMessageBubble(
|
||||||
|
message: Message,
|
||||||
|
audioUrl: String,
|
||||||
|
isMe: Boolean,
|
||||||
|
bubbleColor: Color,
|
||||||
|
viewModel: ChatViewModel // 🎯 傳入 ViewModel
|
||||||
|
) {
|
||||||
|
// 訂閱全域的播放狀態
|
||||||
|
val playbackState by viewModel.audioState.collectAsState()
|
||||||
|
|
||||||
|
// 判斷當前這顆氣泡是不是正在被操作的音訊
|
||||||
|
val isCurrentAudio = playbackState.currentPlayingMessageId == message.id
|
||||||
|
val isPlaying = isCurrentAudio && playbackState.isPlaying
|
||||||
|
|
||||||
|
// 計算當前進度 (0.0 ~ 1.0)
|
||||||
|
val currentPos = if (isCurrentAudio) playbackState.currentPosition else 0L
|
||||||
|
val totalDuration = if (isCurrentAudio) playbackState.duration else 0L
|
||||||
|
val sliderProgress = if (totalDuration > 0) currentPos.toFloat() / totalDuration.toFloat() else 0f
|
||||||
|
|
||||||
|
Row(modifier = Modifier
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = 12.dp, topEnd = 12.dp,
|
||||||
|
bottomStart = if (isMe) 12.dp else 0.dp,
|
||||||
|
bottomEnd = if (isMe) 0.dp else 12.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.background(bubbleColor)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
// 播放 / 暫停按鈕
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.toggleAudioPlayback(message.id, audioUrl) }, // 🎯 觸發全域控制
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
|
||||||
|
contentDescription = if (isPlaying) "暫停" else "播放",
|
||||||
|
tint = if (isMe) Color.White else Color.Black,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 進度條
|
||||||
|
Slider(
|
||||||
|
value = sliderProgress,
|
||||||
|
onValueChange = { ratio ->
|
||||||
|
if (isCurrentAudio) viewModel.seekAudioTo(ratio) // 🎯 允許使用者拖拉進度
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = if (isMe) Color.White else MaterialTheme.colorScheme.primary,
|
||||||
|
activeTrackColor = if (isMe) Color.White else MaterialTheme.colorScheme.primary,
|
||||||
|
inactiveTrackColor = (if (isMe) Color.White else Color.Gray).copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 時間格式化 (00:00)
|
||||||
|
val displayTime = remember(currentPos, totalDuration) {
|
||||||
|
val timeToFormat = if (currentPos > 0) currentPos else totalDuration
|
||||||
|
formatDuration(timeToFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = displayTime,
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
|
||||||
|
color = if (isMe) Color.White.copy(alpha = 0.8f) else Color.Gray,
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -80,6 +80,7 @@ import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.decode.VideoFrameDecoder
|
import coil.decode.VideoFrameDecoder
|
||||||
import com.example.login_v3.data.api.api_class.CallLogInfo
|
import com.example.login_v3.data.api.api_class.CallLogInfo
|
||||||
|
import com.example.login_v3.home.Message.UI.Detail.Message_Component.AudioMessageBubble
|
||||||
import com.example.login_v3.home.Message.UI.Detail.Message_Component.CallLogBubble
|
import com.example.login_v3.home.Message.UI.Detail.Message_Component.CallLogBubble
|
||||||
import com.example.login_v3.home.Message.UI.Detail.Message_Component.ImageLightbox
|
import com.example.login_v3.home.Message.UI.Detail.Message_Component.ImageLightbox
|
||||||
import com.example.login_v3.home.Message.UI.Detail.Message_Component.UserStatusDot
|
import com.example.login_v3.home.Message.UI.Detail.Message_Component.UserStatusDot
|
||||||
|
|
@ -431,6 +432,7 @@ fun MessageMessaging(
|
||||||
partnerDisplayName = state.roomTitle,
|
partnerDisplayName = state.roomTitle,
|
||||||
activeEmojiMessageId = emojiTargetMessage?.id, // ✨ 新增短按目標 ID
|
activeEmojiMessageId = emojiTargetMessage?.id, // ✨ 新增短按目標 ID
|
||||||
activeActionMessageId = actionMessage?.id,
|
activeActionMessageId = actionMessage?.id,
|
||||||
|
viewModel = viewModel,
|
||||||
onReplyClick = { message ->
|
onReplyClick = { message ->
|
||||||
emojiTargetMessage = null
|
emojiTargetMessage = null
|
||||||
viewModel.setActionMessage(message)
|
viewModel.setActionMessage(message)
|
||||||
|
|
@ -536,6 +538,7 @@ fun MessageList(
|
||||||
messages: List<Message>,
|
messages: List<Message>,
|
||||||
activeEmojiMessageId: String?,
|
activeEmojiMessageId: String?,
|
||||||
activeActionMessageId: String?,
|
activeActionMessageId: String?,
|
||||||
|
viewModel: ChatViewModel,
|
||||||
onReplyClick: (Message) -> Unit,
|
onReplyClick: (Message) -> Unit,
|
||||||
onReactionClick: (Message, String) -> Unit,
|
onReactionClick: (Message, String) -> Unit,
|
||||||
onRowClick: (Message) -> Unit,
|
onRowClick: (Message) -> Unit,
|
||||||
|
|
@ -564,7 +567,8 @@ fun MessageList(
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
onReplyClick = onReplyClick,
|
onReplyClick = onReplyClick,
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onRowClick = onRowClick
|
onRowClick = onRowClick,
|
||||||
|
viewModel = viewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -579,6 +583,7 @@ fun MessageRow(
|
||||||
partnerDisplayName: String,
|
partnerDisplayName: String,
|
||||||
isHighlight: Boolean,
|
isHighlight: Boolean,
|
||||||
currentUserId: String,
|
currentUserId: String,
|
||||||
|
viewModel: ChatViewModel,
|
||||||
onReplyClick: (Message) -> Unit,
|
onReplyClick: (Message) -> Unit,
|
||||||
onReactionClick: (Message, String) -> Unit,
|
onReactionClick: (Message, String) -> Unit,
|
||||||
onRowClick: (Message) -> Unit,
|
onRowClick: (Message) -> Unit,
|
||||||
|
|
@ -785,6 +790,33 @@ fun MessageRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//isAudio
|
||||||
|
isAudio -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 250.dp) // 音訊氣泡設定稍窄,更像一般通訊軟體
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = { onReplyClick(message) },
|
||||||
|
onClick = { /* 整塊氣泡點擊邏輯,可留空 */ }
|
||||||
|
),
|
||||||
|
horizontalAlignment = if (isMe) Alignment.End else Alignment.Start
|
||||||
|
) {
|
||||||
|
// 如果有回覆別人的訊息,一樣顯示回覆預覽
|
||||||
|
message.repliedMessage?.let { replied ->
|
||||||
|
RepliedMessagePreview(replied, isMe, partnerDisplayName, currentUserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 呼叫你的音訊播放器元件
|
||||||
|
AudioMessageBubble(
|
||||||
|
message = message,
|
||||||
|
audioUrl = finalMediaUrl,
|
||||||
|
isMe = isMe,
|
||||||
|
bubbleColor = finalBubbleColor,
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 核心分流 3:通話紀錄
|
// 核心分流 3:通話紀錄
|
||||||
callLog != null -> {
|
callLog != null -> {
|
||||||
Box(
|
Box(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue