add delta sync for dm-v12
This commit is contained in:
parent
ce9c146576
commit
76229174c0
3 changed files with 107 additions and 6 deletions
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ import androidx.compose.material.icons.filled.ContactPage
|
||||||
import androidx.compose.material.icons.filled.Description
|
import androidx.compose.material.icons.filled.Description
|
||||||
import androidx.compose.material.icons.filled.Image
|
import androidx.compose.material.icons.filled.Image
|
||||||
import androidx.compose.material.icons.filled.LocationOn
|
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.material3.Divider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
|
@ -69,6 +71,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
// 1. 定義目前支援的選單類型
|
// 1. 定義目前支援的選單類型
|
||||||
enum class AttachmentType(val title: String) {
|
enum class AttachmentType(val title: String) {
|
||||||
MEDIA("Media"),
|
MEDIA("Media"),
|
||||||
|
VIDEO("Video"),
|
||||||
AUDIO("Audio"),
|
AUDIO("Audio"),
|
||||||
DOCUMENT("Document"),
|
DOCUMENT("Document"),
|
||||||
LOCATION("Location"),
|
LOCATION("Location"),
|
||||||
|
|
@ -91,6 +94,7 @@ data class AttachmentOption(
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageAttachmentBar(
|
fun MessageAttachmentBar(
|
||||||
onImageSelected: (Uri) -> Unit,
|
onImageSelected: (Uri) -> Unit,
|
||||||
|
onVideoSelected: (Uri) -> Unit,
|
||||||
onAudioSelected: (Uri) -> Unit, // 🎯 新增:音訊選取回呼
|
onAudioSelected: (Uri) -> Unit, // 🎯 新增:音訊選取回呼
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onDocumentClick: () -> Unit = {},
|
onDocumentClick: () -> Unit = {},
|
||||||
|
|
@ -104,6 +108,7 @@ fun MessageAttachmentBar(
|
||||||
|
|
||||||
// 資料狀態
|
// 資料狀態
|
||||||
val localImages = remember { mutableStateListOf<Uri>() }
|
val localImages = remember { mutableStateListOf<Uri>() }
|
||||||
|
val localVideos = remember { mutableStateListOf<Uri>() }
|
||||||
val localAudios = remember { mutableStateListOf<AudioItem>() }
|
val localAudios = remember { mutableStateListOf<AudioItem>() }
|
||||||
|
|
||||||
var refreshTrigger by remember { mutableStateOf(0) }
|
var refreshTrigger by remember { mutableStateOf(0) }
|
||||||
|
|
@ -112,6 +117,7 @@ fun MessageAttachmentBar(
|
||||||
val attachmentOptions = remember {
|
val attachmentOptions = remember {
|
||||||
listOf(
|
listOf(
|
||||||
AttachmentOption("圖片", Icons.Default.Image, { currentType = AttachmentType.MEDIA }),
|
AttachmentOption("圖片", Icons.Default.Image, { currentType = AttachmentType.MEDIA }),
|
||||||
|
AttachmentOption("影片", Icons.Default.VideoCall, { currentType = AttachmentType.VIDEO }),
|
||||||
AttachmentOption("音訊", Icons.Default.Audiotrack, { currentType = AttachmentType.AUDIO }),
|
AttachmentOption("音訊", Icons.Default.Audiotrack, { currentType = AttachmentType.AUDIO }),
|
||||||
AttachmentOption("文件", Icons.Default.Description, { currentType = AttachmentType.DOCUMENT; onDocumentClick() }),
|
AttachmentOption("文件", Icons.Default.Description, { currentType = AttachmentType.DOCUMENT; onDocumentClick() }),
|
||||||
AttachmentOption("位置", Icons.Default.LocationOn, { currentType = AttachmentType.LOCATION; onLocationClick() }),
|
AttachmentOption("位置", Icons.Default.LocationOn, { currentType = AttachmentType.LOCATION; onLocationClick() }),
|
||||||
|
|
@ -119,12 +125,14 @@ fun MessageAttachmentBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Android 14+ 圖片部分權限檢查
|
// Android 14+ 媒體部分權限檢查
|
||||||
val isPartialAccess by remember(refreshTrigger) {
|
val isPartialAccess by remember(refreshTrigger) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED &&
|
val partial = 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 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 {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -157,6 +165,28 @@ fun MessageAttachmentBar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎯 新增:讀取影片的 Effect
|
||||||
|
LaunchedEffect(refreshTrigger) {
|
||||||
|
val videoUris = mutableListOf<Uri>()
|
||||||
|
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)
|
// 🎯 4. 新增:讀取音訊的 Effect (記得在 AndroidManifest 宣告 READ_EXTERNAL_STORAGE 或 READ_MEDIA_AUDIO)
|
||||||
LaunchedEffect(refreshTrigger) {
|
LaunchedEffect(refreshTrigger) {
|
||||||
val audioItems = mutableListOf<AudioItem>()
|
val audioItems = mutableListOf<AudioItem>()
|
||||||
|
|
@ -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 -> {
|
AttachmentType.AUDIO -> {
|
||||||
// 🎯 5. 新增:音訊渲染 UI (音訊檔名較長,改用垂直捲動清單比網格好看)
|
// 🎯 5. 新增:音訊渲染 UI (音訊檔名較長,改用垂直捲動清單比網格好看)
|
||||||
if (localAudios.isEmpty()) {
|
if (localAudios.isEmpty()) {
|
||||||
|
|
@ -360,6 +451,7 @@ fun MessageAttachmentBar(
|
||||||
// 🎯 6. 稍微優化:如果選中當前項目,給予特別的高亮白底
|
// 🎯 6. 稍微優化:如果選中當前項目,給予特別的高亮白底
|
||||||
val isSelected = option.title == when(currentType) {
|
val isSelected = option.title == when(currentType) {
|
||||||
AttachmentType.MEDIA -> "圖片"
|
AttachmentType.MEDIA -> "圖片"
|
||||||
|
AttachmentType.VIDEO -> "影片"
|
||||||
AttachmentType.AUDIO -> "音訊"
|
AttachmentType.AUDIO -> "音訊"
|
||||||
AttachmentType.DOCUMENT -> "文件"
|
AttachmentType.DOCUMENT -> "文件"
|
||||||
AttachmentType.LOCATION -> "位置"
|
AttachmentType.LOCATION -> "位置"
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ fun MessageMessaging(
|
||||||
val requiredPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val requiredPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Manifest.permission.READ_MEDIA_IMAGES,
|
Manifest.permission.READ_MEDIA_IMAGES,
|
||||||
|
Manifest.permission.READ_MEDIA_VIDEO,
|
||||||
Manifest.permission.READ_MEDIA_AUDIO
|
Manifest.permission.READ_MEDIA_AUDIO
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -207,11 +208,12 @@ fun MessageMessaging(
|
||||||
// 【Android 14+】
|
// 【Android 14+】
|
||||||
// 音訊必須要過
|
// 音訊必須要過
|
||||||
val audioGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_AUDIO) == PackageManager.PERMISSION_GRANTED
|
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 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 {
|
} else {
|
||||||
// 【Android 13 及以下】
|
// 【Android 13 及以下】
|
||||||
requiredPermissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
|
requiredPermissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
|
||||||
|
|
@ -493,6 +495,11 @@ fun MessageMessaging(
|
||||||
// 上傳後自動退回輸入框
|
// 上傳後自動退回輸入框
|
||||||
localBottomBarState = null
|
localBottomBarState = null
|
||||||
},
|
},
|
||||||
|
onVideoSelected = { uri ->
|
||||||
|
// 🎯 使用者選取了影片,觸發上傳
|
||||||
|
viewModel.uploadAttachment(roomId, uri)
|
||||||
|
localBottomBarState = null
|
||||||
|
},
|
||||||
onAudioSelected = { uri ->
|
onAudioSelected = { uri ->
|
||||||
// 處理音訊上傳的邏輯(通常跟圖片上傳類似,或是呼叫你專門處理音訊的函式)
|
// 處理音訊上傳的邏輯(通常跟圖片上傳類似,或是呼叫你專門處理音訊的函式)
|
||||||
viewModel.uploadAttachment(roomId, uri) // 假設你的 ViewModel 上傳函式是通用的
|
viewModel.uploadAttachment(roomId, uri) // 假設你的 ViewModel 上傳函式是通用的
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue