From 78c3c7f21814cfee01fafedc7580285beb914b93 Mon Sep 17 00:00:00 2001 From: Eric85710 Date: Mon, 22 Jun 2026 18:10:07 +0800 Subject: [PATCH] dm-145 --- app/build.gradle.kts | 5 ++ .../Detail/Message_Component/Video_Player.kt | 71 +++++++++++++++++++ .../Message/UI/Detail/Message_Messaging.kt | 50 +++++++++---- 3 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Video_Player.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f9bcaf9..e10b8bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Video_Player.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Video_Player.kt new file mode 100644 index 0000000..6eb4454 --- /dev/null +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Video_Player.kt @@ -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() + ) + } +} \ 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 4e0da2a..6c0ec5e 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 @@ -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(null) } + //inspect video + var lightboxVideoUrl by remember { mutableStateOf(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