From 76229174c0dc3c1345bf5b552fb2a8c693c98014 Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 5 Aug 2026 17:54:18 +0800 Subject: [PATCH] add delta sync for dm-v12 --- app/src/main/AndroidManifest.xml | 2 + .../Message_Component/Attachment_Bar.kt | 98 ++++++++++++++++++- .../Message/UI/Detail/Message_Messaging.kt | 13 ++- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e72f1e1..65e42a8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,8 @@ + + diff --git a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Attachment_Bar.kt b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Attachment_Bar.kt index 2bc9b7d..37e8318 100644 --- a/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Attachment_Bar.kt +++ b/app/src/main/java/com/example/login_v3/home/Message/UI/Detail/Message_Component/Attachment_Bar.kt @@ -56,6 +56,8 @@ import androidx.compose.material.icons.filled.ContactPage import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.VideoCall import androidx.compose.material3.Divider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf @@ -69,6 +71,7 @@ import androidx.compose.ui.text.style.TextOverflow // 1. 定義目前支援的選單類型 enum class AttachmentType(val title: String) { MEDIA("Media"), + VIDEO("Video"), AUDIO("Audio"), DOCUMENT("Document"), LOCATION("Location"), @@ -91,6 +94,7 @@ data class AttachmentOption( @Composable fun MessageAttachmentBar( onImageSelected: (Uri) -> Unit, + onVideoSelected: (Uri) -> Unit, onAudioSelected: (Uri) -> Unit, // 🎯 新增:音訊選取回呼 onCancel: () -> Unit, onDocumentClick: () -> Unit = {}, @@ -104,6 +108,7 @@ fun MessageAttachmentBar( // 資料狀態 val localImages = remember { mutableStateListOf() } + val localVideos = remember { mutableStateListOf() } val localAudios = remember { mutableStateListOf() } var refreshTrigger by remember { mutableStateOf(0) } @@ -112,6 +117,7 @@ fun MessageAttachmentBar( val attachmentOptions = remember { listOf( AttachmentOption("圖片", Icons.Default.Image, { currentType = AttachmentType.MEDIA }), + AttachmentOption("影片", Icons.Default.VideoCall, { currentType = AttachmentType.VIDEO }), AttachmentOption("音訊", Icons.Default.Audiotrack, { currentType = AttachmentType.AUDIO }), AttachmentOption("文件", Icons.Default.Description, { currentType = AttachmentType.DOCUMENT; onDocumentClick() }), AttachmentOption("位置", Icons.Default.LocationOn, { currentType = AttachmentType.LOCATION; onLocationClick() }), @@ -119,12 +125,14 @@ fun MessageAttachmentBar( ) } - // Android 14+ 圖片部分權限檢查 + // Android 14+ 媒體部分權限檢查 val isPartialAccess by remember(refreshTrigger) { derivedStateOf { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED + val partial = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED + val imageFull = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED + val videoFull = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED + partial && (!imageFull || !videoFull) } else { false } @@ -157,6 +165,28 @@ fun MessageAttachmentBar( } } + // 🎯 新增:讀取影片的 Effect + LaunchedEffect(refreshTrigger) { + val videoUris = mutableListOf() + val projection = arrayOf(MediaStore.Video.Media._ID, MediaStore.Video.Media.DATE_TAKEN) + val sortOrder = "${MediaStore.Video.Media.DATE_TAKEN} DESC" + try { + context.contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection, null, null, sortOrder)?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID) + var count = 0 + while (cursor.moveToNext() && count < 24) { + val id = cursor.getLong(idColumn) + videoUris.add(ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id)) + count++ + } + } + localVideos.clear() + localVideos.addAll(videoUris) + } catch (e: SecurityException) { + localVideos.clear() + } + } + // 🎯 4. 新增:讀取音訊的 Effect (記得在 AndroidManifest 宣告 READ_EXTERNAL_STORAGE 或 READ_MEDIA_AUDIO) LaunchedEffect(refreshTrigger) { val audioItems = mutableListOf() @@ -295,6 +325,67 @@ fun MessageAttachmentBar( } } + AttachmentType.VIDEO -> { + // 影片網格邏輯 + if (localVideos.isEmpty() && !isPartialAccess) { + EmptyStateView("無最近影片檔案") + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (isPartialAccess) { + item { + Box( + modifier = Modifier + .aspectRatio(1f) + .clip(RoundedCornerShape(12.dp)) + .background(Color.White.copy(alpha = 0.1f)) + .clickable { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + permissionLauncher.launch(Manifest.permission.READ_MEDIA_VIDEO) + } + }, + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) { + Icon(imageVector = Icons.Default.Add, contentDescription = "管理影片", tint = Color.White) + Text("管理檔案", color = Color.White, style = MaterialTheme.typography.labelSmall) + } + } + } + } + + items(localVideos) { uri -> + Box( + modifier = Modifier + .aspectRatio(1f) + .clip(RoundedCornerShape(12.dp)) + .clickable { onVideoSelected(uri) } + ) { + AsyncImage( + model = uri, + contentDescription = "影片預覽", + modifier = Modifier.fillMaxSize(), + contentScale = androidx.compose.ui.layout.ContentScale.Crop + ) + // 影片圖示疊加 + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = null, + tint = Color.White.copy(alpha = 0.8f), + modifier = Modifier + .size(32.dp) + .align(Alignment.Center) + ) + } + } + } + } + } + AttachmentType.AUDIO -> { // 🎯 5. 新增:音訊渲染 UI (音訊檔名較長,改用垂直捲動清單比網格好看) if (localAudios.isEmpty()) { @@ -360,6 +451,7 @@ fun MessageAttachmentBar( // 🎯 6. 稍微優化:如果選中當前項目,給予特別的高亮白底 val isSelected = option.title == when(currentType) { AttachmentType.MEDIA -> "圖片" + AttachmentType.VIDEO -> "影片" AttachmentType.AUDIO -> "音訊" AttachmentType.DOCUMENT -> "文件" AttachmentType.LOCATION -> "位置" 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 bb25c7f..984bc32 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 @@ -195,6 +195,7 @@ fun MessageMessaging( val requiredPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { arrayOf( Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.READ_MEDIA_AUDIO ) } else { @@ -207,11 +208,12 @@ fun MessageMessaging( // 【Android 14+】 // 音訊必須要過 val audioGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_AUDIO) == PackageManager.PERMISSION_GRANTED - // 圖片不管是「全過(READ_MEDIA_IMAGES)」還是「部分過(READ_MEDIA_VISUAL_USER_SELECTED)」都可以算過! + // 圖片/影片不管是「全過」還是「部分過」都可以算過! val imageGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED - val partialImageGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED + val videoGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED + val partialVisualGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED - audioGranted && (imageGranted || partialImageGranted) + audioGranted && (imageGranted || videoGranted || partialVisualGranted) } else { // 【Android 13 及以下】 requiredPermissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } @@ -493,6 +495,11 @@ fun MessageMessaging( // 上傳後自動退回輸入框 localBottomBarState = null }, + onVideoSelected = { uri -> + // 🎯 使用者選取了影片,觸發上傳 + viewModel.uploadAttachment(roomId, uri) + localBottomBarState = null + }, onAudioSelected = { uri -> // 處理音訊上傳的邏輯(通常跟圖片上傳類似,或是呼叫你專門處理音訊的函式) viewModel.uploadAttachment(roomId, uri) // 假設你的 ViewModel 上傳函式是通用的