This commit is contained in:
Eric85710 2026-06-24 16:22:43 +08:00
parent 18065034b7
commit cfce2b3045
3 changed files with 147 additions and 23 deletions

View file

@ -3,6 +3,12 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<application <application
android:name=".MyApplication" android:name=".MyApplication"
android:allowBackup="true" android:allowBackup="true"
@ -18,7 +24,6 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MyApp.Splash"> android:theme="@style/Theme.MyApp.Splash">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View file

@ -1,10 +1,13 @@
package com.example.login_v3.home.Message.UI.Detail.Message_Component package com.example.login_v3.home.Message.UI.Detail.Message_Component
import android.content.ContentUris
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -13,7 +16,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Image
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -23,19 +25,95 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import android.provider.MediaStore
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import coil.compose.AsyncImage // 建議使用 Coil 來載入本地圖片 Uri,效能極佳
import android.Manifest
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.filled.Add
@Composable @Composable
fun MessageAttachmentBar( fun MessageAttachmentBar(
onImageClick: () -> Unit, onImageSelected: (Uri) -> Unit, // 🎯 改為直接回傳選中的圖片 Uri
onCancel: () -> Unit onCancel: () -> Unit
) { ) {
val context = LocalContext.current
val isPartialAccess by remember {
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
} else {
false
}
}
}
// 處理 Android 14 重新調整選取照片的 Launcher
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { _ ->
// 重新讀取或觸發相簿狀態更新(這裡會觸發 localImages 重新 query)
}
// 💡 非同步讀取手機內最新的數張圖片 Uri
val localImages by remember {
derivedStateOf {
val imageUris = mutableListOf<Uri>()
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DATE_TAKEN
)
// 依時間倒序排序,讓最新的照片排在最前面
val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
sortOrder
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
// 這裡限制讀取前 20 張,避免一次載入太多有效能隱憂
var count = 0
while (cursor.moveToNext() && count < 20) {
val id = cursor.getLong(idColumn)
val contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
imageUris.add(contentUri)
count++
}
}
imageUris
}
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.navigationBarsPadding() .navigationBarsPadding()
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
) { ) {
// 毛玻璃背景與邊框 (與 InputBar 一致) // 毛玻璃背景與邊框 (保持與 InputBar 一致)
Box(modifier = Modifier.matchParentSize().blur(10.dp)) Box(modifier = Modifier.matchParentSize().blur(10.dp))
Box( Box(
modifier = Modifier modifier = Modifier
@ -47,29 +125,69 @@ fun MessageAttachmentBar(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 12.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
horizontalArrangement = Arrangement.spacedBy(24.dp)
) { ) {
// 圖片選擇按鈕
IconButton(onClick = onImageClick) { // 🎯 橫向媒體檢視器
Column(horizontalAlignment = Alignment.CenterHorizontally) { LazyRow(
Icon( modifier = Modifier.weight(1f).height(70.dp),
imageVector = Icons.Default.Image, // 記得引入對應的 Icon horizontalArrangement = Arrangement.spacedBy(8.dp),
contentDescription = "選擇圖片", verticalAlignment = Alignment.CenterVertically
tint = Color.White ) {
) // 🎯 核心優化:如果是 Android 14 部分允許,在最左邊固定顯示一個「編輯存取範圍」的按鈕
if (isPartialAccess) {
item {
Box(
modifier = Modifier
.size(70.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color.White.copy(alpha = 0.1f))
.clickable {
// 再次要求 READ_MEDIA_IMAGES,系統會自動彈出「管理已選取的照片」視窗
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
permissionLauncher.launch(Manifest.permission.READ_MEDIA_IMAGES)
}
},
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(imageVector = Icons.Default.Add, contentDescription = "管理照片", tint = Color.White)
Text("管理", color = Color.White, style = MaterialTheme.typography.bodySmall)
}
}
}
}
if (localImages.isEmpty()) {
item {
Text(text = "無媒體檔案", color = Color.White.copy(alpha = 0.6f), modifier = Modifier.padding(start = 4.dp))
}
} else {
items(localImages) { uri ->
AsyncImage(
model = uri,
contentDescription = "媒體預覽",
modifier = Modifier.size(70.dp).clip(RoundedCornerShape(8.dp)).clickable { onImageSelected(uri) },
contentScale = androidx.compose.ui.layout.ContentScale.Crop
)
}
} }
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.width(12.dp))
// 關閉/返回輸入框按鈕 // 關閉/返回輸入框按鈕
IconButton(onClick = onCancel) { IconButton(
onClick = onCancel,
modifier = Modifier
.size(36.dp)
.background(Color.White.copy(alpha = 0.15f), CircleShape)
) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "返回輸入", contentDescription = "返回輸入",
tint = Color.White.copy(alpha = 0.7f) tint = Color.White
) )
} }
} }

View file

@ -427,13 +427,14 @@ fun MessageMessaging(
is BottomBarState.AttachmentMenu -> { is BottomBarState.AttachmentMenu -> {
MessageAttachmentBar( MessageAttachmentBar(
onImageClick = { onImageSelected = { uri ->
imagePickerLauncher.launch("image/*") // 🎯 使用者在預覽列中點選了某張圖片,直接觸發上傳
// 🎯 選完圖片後,將本地狀態重置,自動回歸 Input 輸入框 viewModel.uploadAttachment(roomId, uri)
// 上傳後自動退回輸入框
localBottomBarState = null localBottomBarState = null
}, },
onCancel = { onCancel = {
// 🎯 點擊關閉,同樣回歸 Input 輸入框 // 點擊關閉退回輸入框
localBottomBarState = null localBottomBarState = null
} }
) )