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