feat: Add camera button to MediaGrid for quick access
This commit is contained in:
@@ -120,6 +120,12 @@ dependencies {
|
|||||||
// Biometric authentication
|
// Biometric authentication
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
|
|
||||||
|
// CameraX for camera preview
|
||||||
|
implementation("androidx.camera:camera-core:1.3.1")
|
||||||
|
implementation("androidx.camera:camera-camera2:1.3.1")
|
||||||
|
implementation("androidx.camera:camera-lifecycle:1.3.1")
|
||||||
|
implementation("androidx.camera:camera-view:1.3.1")
|
||||||
|
|
||||||
// Baseline Profiles for startup performance
|
// Baseline Profiles for startup performance
|
||||||
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
||||||
|
|
||||||
|
|||||||
@@ -356,32 +356,26 @@ fun UnlockScreen(
|
|||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn(tween(400)) + scaleIn(tween(400, easing = FastOutSlowInEasing))
|
enter = fadeIn(tween(400)) + scaleIn(tween(400, easing = FastOutSlowInEasing))
|
||||||
) {
|
) {
|
||||||
Box(
|
if (selectedAccount != null) {
|
||||||
modifier = Modifier
|
val database = RosettaDatabase.getDatabase(context)
|
||||||
.size(120.dp)
|
val avatarRepository = remember(selectedAccount!!.publicKey) {
|
||||||
.clip(RoundedCornerShape(28.dp))
|
AvatarRepository(context, database.avatarDao(), selectedAccount!!.publicKey)
|
||||||
.background(
|
}
|
||||||
if (selectedAccount != null) {
|
AvatarImage(
|
||||||
val colors = getAvatarColor(selectedAccount!!.publicKey, isDarkTheme)
|
publicKey = selectedAccount!!.publicKey,
|
||||||
colors.backgroundColor
|
avatarRepository = avatarRepository,
|
||||||
} else {
|
size = 120.dp,
|
||||||
cardBackground
|
isDarkTheme = isDarkTheme,
|
||||||
}
|
shape = RoundedCornerShape(28.dp)
|
||||||
),
|
)
|
||||||
contentAlignment = Alignment.Center
|
} else {
|
||||||
) {
|
Box(
|
||||||
if (selectedAccount != null) {
|
modifier = Modifier
|
||||||
val database = RosettaDatabase.getDatabase(context)
|
.size(120.dp)
|
||||||
val avatarRepository = remember(selectedAccount!!.publicKey) {
|
.clip(RoundedCornerShape(28.dp))
|
||||||
AvatarRepository(context, database.avatarDao(), selectedAccount!!.publicKey)
|
.background(cardBackground),
|
||||||
}
|
contentAlignment = Alignment.Center
|
||||||
AvatarImage(
|
) {
|
||||||
publicKey = selectedAccount!!.publicKey,
|
|
||||||
avatarRepository = avatarRepository,
|
|
||||||
size = 120.dp,
|
|
||||||
isDarkTheme = isDarkTheme
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
Text(
|
||||||
text = "?",
|
text = "?",
|
||||||
fontSize = 48.sp,
|
fontSize = 48.sp,
|
||||||
|
|||||||
@@ -30,10 +30,16 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
@@ -112,10 +118,14 @@ fun MediaPickerBottomSheet(
|
|||||||
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val permissions = 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_VIDEO,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
|
arrayOf(
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPermission = permissions.all {
|
hasPermission = permissions.all {
|
||||||
@@ -211,10 +221,14 @@ fun MediaPickerBottomSheet(
|
|||||||
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val permissions = 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_VIDEO,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
|
arrayOf(
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
)
|
||||||
}
|
}
|
||||||
permissionLauncher.launch(permissions)
|
permissionLauncher.launch(permissions)
|
||||||
}
|
}
|
||||||
@@ -262,6 +276,10 @@ fun MediaPickerBottomSheet(
|
|||||||
MediaGrid(
|
MediaGrid(
|
||||||
mediaItems = mediaItems,
|
mediaItems = mediaItems,
|
||||||
selectedItems = selectedItems,
|
selectedItems = selectedItems,
|
||||||
|
onCameraClick = {
|
||||||
|
onDismiss()
|
||||||
|
onOpenCamera()
|
||||||
|
},
|
||||||
onItemClick = { item ->
|
onItemClick = { item ->
|
||||||
// Telegram-style:
|
// Telegram-style:
|
||||||
// - Первый клик по невыбранной фото → выбрать
|
// - Первый клик по невыбранной фото → выбрать
|
||||||
@@ -465,6 +483,7 @@ private fun QuickActionButton(
|
|||||||
private fun MediaGrid(
|
private fun MediaGrid(
|
||||||
mediaItems: List<MediaItem>,
|
mediaItems: List<MediaItem>,
|
||||||
selectedItems: Set<Long>,
|
selectedItems: Set<Long>,
|
||||||
|
onCameraClick: () -> Unit,
|
||||||
onItemClick: (MediaItem) -> Unit,
|
onItemClick: (MediaItem) -> Unit,
|
||||||
onItemLongClick: (MediaItem) -> Unit,
|
onItemLongClick: (MediaItem) -> Unit,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
@@ -480,6 +499,15 @@ private fun MediaGrid(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
|
// 📷 Camera button as first item
|
||||||
|
item(key = "camera_button") {
|
||||||
|
CameraGridItem(
|
||||||
|
onClick = onCameraClick,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media items
|
||||||
items(
|
items(
|
||||||
items = mediaItems,
|
items = mediaItems,
|
||||||
key = { it.id }
|
key = { it.id }
|
||||||
@@ -498,6 +526,118 @@ private fun MediaGrid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 📷 Camera button in grid - first item with live preview
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun CameraGridItem(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
isDarkTheme: Boolean
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
||||||
|
|
||||||
|
// Check if camera permission is granted - use mutableState for reactivity
|
||||||
|
var hasCameraPermission by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-check permission when component becomes visible
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
hasCameraPermission = ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(backgroundColor)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (hasCameraPermission) {
|
||||||
|
// Show live camera preview
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
val previewView = PreviewView(ctx).apply {
|
||||||
|
scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
|
||||||
|
cameraProviderFuture.addListener({
|
||||||
|
try {
|
||||||
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
|
|
||||||
|
val preview = Preview.Builder().build().also {
|
||||||
|
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use back camera
|
||||||
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
cameraProvider.bindToLifecycle(
|
||||||
|
lifecycleOwner,
|
||||||
|
cameraSelector,
|
||||||
|
preview
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Camera init failed
|
||||||
|
}
|
||||||
|
}, ContextCompat.getMainExecutor(ctx))
|
||||||
|
|
||||||
|
previewView
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Camera icon overlay
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.3f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.Camera,
|
||||||
|
contentDescription = "Camera",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No permission - show placeholder
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = TablerIcons.Camera,
|
||||||
|
contentDescription = "Camera",
|
||||||
|
tint = PrimaryBlue,
|
||||||
|
modifier = Modifier.size(40.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Camera",
|
||||||
|
color = PrimaryBlue,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MediaGridItem(
|
private fun MediaGridItem(
|
||||||
item: MediaItem,
|
item: MediaItem,
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -44,6 +46,7 @@ import kotlinx.coroutines.withContext
|
|||||||
* @param onClick Обработчик клика (опционально)
|
* @param onClick Обработчик клика (опционально)
|
||||||
* @param showOnlineIndicator Показывать индикатор онлайн
|
* @param showOnlineIndicator Показывать индикатор онлайн
|
||||||
* @param isOnline Пользователь онлайн
|
* @param isOnline Пользователь онлайн
|
||||||
|
* @param shape Форма аватара (по умолчанию круг)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AvatarImage(
|
fun AvatarImage(
|
||||||
@@ -53,7 +56,8 @@ fun AvatarImage(
|
|||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
showOnlineIndicator: Boolean = false,
|
showOnlineIndicator: Boolean = false,
|
||||||
isOnline: Boolean = false
|
isOnline: Boolean = false,
|
||||||
|
shape: Shape = CircleShape
|
||||||
) {
|
) {
|
||||||
// Получаем аватары из репозитория
|
// Получаем аватары из репозитория
|
||||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||||
@@ -85,7 +89,7 @@ fun AvatarImage(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(size)
|
.size(size)
|
||||||
.clip(CircleShape)
|
.clip(shape)
|
||||||
.then(
|
.then(
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
Modifier.clickable(onClick = onClick)
|
Modifier.clickable(onClick = onClick)
|
||||||
@@ -113,7 +117,8 @@ fun AvatarImage(
|
|||||||
AvatarPlaceholder(
|
AvatarPlaceholder(
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
size = size,
|
size = size,
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme,
|
||||||
|
shape = shape
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +142,8 @@ fun AvatarPlaceholder(
|
|||||||
publicKey: String,
|
publicKey: String,
|
||||||
size: Dp = 40.dp,
|
size: Dp = 40.dp,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
fontSize: TextUnit? = null
|
fontSize: TextUnit? = null,
|
||||||
|
shape: Shape = CircleShape
|
||||||
) {
|
) {
|
||||||
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
|
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
|
||||||
val avatarText = getAvatarText(publicKey)
|
val avatarText = getAvatarText(publicKey)
|
||||||
@@ -145,7 +151,7 @@ fun AvatarPlaceholder(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(size)
|
.size(size)
|
||||||
.clip(CircleShape)
|
.clip(shape)
|
||||||
.background(avatarColors.backgroundColor),
|
.background(avatarColors.backgroundColor),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user