dm-145
This commit is contained in:
parent
e4db01d592
commit
78c3c7f218
3 changed files with 114 additions and 12 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
// 依據類型,分流塞入對應的狀態
|
||||
if (isImage) {
|
||||
lightboxImageUrl = finalUrl
|
||||
} else {
|
||||
// 🎯 B 狀態:一般文字或通話,維持原本點擊叫出 Emoji Bar 的邏輯
|
||||
lightboxVideoUrl = finalUrl // 🎯 觸發影片全螢幕
|
||||
}
|
||||
} else {
|
||||
// 🎯 一般文字或通話,叫出 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue