From e054c014ad1629c6ef4507996ff90e774ca56cb4 Mon Sep 17 00:00:00 2001 From: Eric85710 Date: Tue, 23 Jun 2026 17:12:33 +0800 Subject: [PATCH] dm-150 --- .../Detail/Message_Component/Audio_bubble.kt | 100 ++++++++++++++++++ .../Message/UI/Detail/Message_Messaging.kt | 34 +++++- 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Audio_bubble.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Audio_bubble.kt index 0211752..f832895 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Audio_bubble.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Audio_bubble.kt @@ -1,2 +1,102 @@ 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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt index 654262f..ab45c09 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Messaging.kt @@ -80,6 +80,7 @@ import androidx.compose.material.icons.filled.PlayArrow import coil.ImageLoader import coil.decode.VideoFrameDecoder 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.ImageLightbox import com.example.login_v3.home.Message.UI.Detail.Message_Component.UserStatusDot @@ -431,6 +432,7 @@ fun MessageMessaging( partnerDisplayName = state.roomTitle, activeEmojiMessageId = emojiTargetMessage?.id, // ✨ 新增短按目標 ID activeActionMessageId = actionMessage?.id, + viewModel = viewModel, onReplyClick = { message -> emojiTargetMessage = null viewModel.setActionMessage(message) @@ -536,6 +538,7 @@ fun MessageList( messages: List, activeEmojiMessageId: String?, activeActionMessageId: String?, + viewModel: ChatViewModel, onReplyClick: (Message) -> Unit, onReactionClick: (Message, String) -> Unit, onRowClick: (Message) -> Unit, @@ -564,7 +567,8 @@ fun MessageList( currentUserId = currentUserId, onReplyClick = onReplyClick, onReactionClick = onReactionClick, - onRowClick = onRowClick + onRowClick = onRowClick, + viewModel = viewModel, ) } } @@ -579,6 +583,7 @@ fun MessageRow( partnerDisplayName: String, isHighlight: Boolean, currentUserId: String, + viewModel: ChatViewModel, onReplyClick: (Message) -> Unit, onReactionClick: (Message, String) -> 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:通話紀錄 callLog != null -> { Box(