This commit is contained in:
Eric85710 2026-06-22 18:10:07 +08:00
parent e4db01d592
commit 78c3c7f218
3 changed files with 114 additions and 12 deletions

View file

@ -99,4 +99,9 @@ dependencies {
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion") // 支援 Flow 必須要加這行
ksp("androidx.room:room-compiler:$roomVersion") // 使用 KSP 來編譯 Room 註解
//video player
val media3Version = "1.5.1" // 建議統一宣告版本,避免版本衝突
implementation("androidx.media3:media3-exoplayer:$media3Version")
implementation("androidx.media3:media3-ui:$media3Version")
}

View file

@ -0,0 +1,71 @@
package com.example.login_v3.home.Message.UI.Detail.Message_Component
import androidx.activity.compose.BackHandler
import androidx.annotation.OptIn
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
@OptIn(UnstableApi::class)
@Composable
fun VideoLightbox(
videoUrl: String,
onDismiss: () -> Unit
) {
BackHandler(enabled = true) {
onDismiss()
}
val context = LocalContext.current
// 初始化 ExoPlayer
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
val mediaItem = MediaItem.fromUri(videoUrl)
setMediaItem(mediaItem)
prepare()
playWhenReady = true // 自動播放
}
}
// 當 Composable 銷毀時,一定要釋放播放器資源以免記憶體流失
DisposableEffect(Unit) {
onDispose {
exoPlayer.release()
}
}
// 全螢幕黑色背景黑燈箱
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.95f))
.clickable { onDismiss() }, // 點擊背景可以關閉
contentAlignment = Alignment.Center
) {
AndroidView(
factory = { ctx ->
PlayerView(ctx).apply {
player = exoPlayer
useController = true // 顯示預設的播放、暫停、進度條控制面板
// 點擊播放器本體時不要觸發外層的關閉事件
setOnClickListener { /* 阻止點擊事件穿透 */ }
}
},
modifier = Modifier.fillMaxSize()
)
}
}

View file

@ -81,6 +81,7 @@ import com.example.login_v3.data.api.api_class.CallLogInfo
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
import com.example.login_v3.home.Message.UI.Detail.Message_Component.VideoLightbox
//bottom bar state
@ -118,6 +119,9 @@ fun MessageMessaging(
//inspect image
var lightboxImageUrl by remember { mutableStateOf<String?>(null) }
//inspect video
var lightboxVideoUrl by remember { mutableStateOf<String?>(null) }
// 🌟 核心:使用 derivedStateOf 統一控管狀態權限(長按優先權通常大於單擊)
val bottomBarState by remember {
derivedStateOf {
@ -431,17 +435,24 @@ fun MessageMessaging(
},
// 💡 單擊事件:開啟 Emoji 選單,同時清空長按選單
onRowClick = { message ->
val isAttachmentImage = message.attachment?.mimeType?.startsWith("image/") == true
val contentLowerCase = (message.content ?: "").lowercase()
val imageExtensions = listOf(".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif")
val isLegacyImage = imageExtensions.any { contentLowerCase.contains(it) }
if (isAttachmentImage || isLegacyImage) {
// 🎯 A 狀態:這是圖片,計算圖片完整 URL 並塞入 lightbox 狀態觸發放大
val rawPath = if (isAttachmentImage && !message.attachment?.filename.isNullOrBlank()) {
message.attachment!!.filename
} else {
message.content ?: ""
//is image
val isAttachmentImage = message.attachment?.mimeType?.startsWith("image/") == true
val imageExtensions = listOf(".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif")
val isImage = isAttachmentImage || imageExtensions.any { contentLowerCase.contains(it) }
//is video
val isAttachmentVideo = message.attachment?.mimeType?.startsWith("video/") == true
val videoExtensions = listOf(".mp4", ".mov", ".mkv", ".avi", ".3gp", ".webm")
val isVideo = isAttachmentVideo || videoExtensions.any { contentLowerCase.contains(it) }
if (isImage || isVideo) {
// 計算乾淨的 URL 網址
val rawPath = when {
isAttachmentImage && !message.attachment?.filename.isNullOrBlank() -> message.attachment!!.filename
isAttachmentVideo && !message.attachment?.filename.isNullOrBlank() -> message.attachment!!.filename
else -> message.content ?: ""
}
val baseUrl = "https://tg.technologia-tw.com"
val finalUrl = when {
@ -457,12 +468,18 @@ fun MessageMessaging(
}
}
// 關閉可能開啟的選單,並打開大圖
// 關閉所有底部的 Emoji 或動作選單
viewModel.clearActionMessage()
emojiTargetMessage = null
lightboxImageUrl = finalUrl
// 依據類型,分流塞入對應的狀態
if (isImage) {
lightboxImageUrl = finalUrl
} else {
lightboxVideoUrl = finalUrl // 🎯 觸發影片全螢幕
}
} else {
// 🎯 B 狀態:一般文字或通話,維持原本點擊叫出 Emoji Bar 的邏輯
// 🎯 一般文字或通話,叫出 Emoji Bar
viewModel.clearActionMessage()
emojiTargetMessage = message
}
@ -492,12 +509,21 @@ fun MessageMessaging(
}
}
//image inspect
lightboxImageUrl?.let { url ->
ImageLightbox(
imageUrl = url,
onDismiss = { lightboxImageUrl = null }
)
}
//video inspect
lightboxVideoUrl?.let { url ->
VideoLightbox(
videoUrl = url,
onDismiss = { lightboxVideoUrl = null }
)
}
}
@Composable