feat: Add camera button to MediaGrid for quick access

This commit is contained in:
k1ngsterr1
2026-01-29 21:54:17 +05:00
parent 5d1ba8144f
commit 8c30fc3549
4 changed files with 181 additions and 35 deletions

View File

@@ -120,6 +120,12 @@ dependencies {
// Biometric authentication
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
implementation("androidx.profileinstaller:profileinstaller:1.3.1")

View File

@@ -355,20 +355,6 @@ fun UnlockScreen(
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400)) + scaleIn(tween(400, easing = FastOutSlowInEasing))
) {
Box(
modifier = Modifier
.size(120.dp)
.clip(RoundedCornerShape(28.dp))
.background(
if (selectedAccount != null) {
val colors = getAvatarColor(selectedAccount!!.publicKey, isDarkTheme)
colors.backgroundColor
} else {
cardBackground
}
),
contentAlignment = Alignment.Center
) {
if (selectedAccount != null) {
val database = RosettaDatabase.getDatabase(context)
@@ -379,9 +365,17 @@ fun UnlockScreen(
publicKey = selectedAccount!!.publicKey,
avatarRepository = avatarRepository,
size = 120.dp,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
shape = RoundedCornerShape(28.dp)
)
} else {
Box(
modifier = Modifier
.size(120.dp)
.clip(RoundedCornerShape(28.dp))
.background(cardBackground),
contentAlignment = Alignment.Center
) {
Text(
text = "?",
fontSize = 48.sp,

View File

@@ -30,10 +30,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
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 coil.compose.AsyncImage
import coil.request.ImageRequest
@@ -112,10 +118,14 @@ fun MediaPickerBottomSheet(
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.CAMERA
)
} else {
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
)
}
hasPermission = permissions.all {
@@ -211,10 +221,14 @@ fun MediaPickerBottomSheet(
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.CAMERA
)
} else {
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
)
}
permissionLauncher.launch(permissions)
}
@@ -262,6 +276,10 @@ fun MediaPickerBottomSheet(
MediaGrid(
mediaItems = mediaItems,
selectedItems = selectedItems,
onCameraClick = {
onDismiss()
onOpenCamera()
},
onItemClick = { item ->
// Telegram-style:
// - Первый клик по невыбранной фото → выбрать
@@ -465,6 +483,7 @@ private fun QuickActionButton(
private fun MediaGrid(
mediaItems: List<MediaItem>,
selectedItems: Set<Long>,
onCameraClick: () -> Unit,
onItemClick: (MediaItem) -> Unit,
onItemLongClick: (MediaItem) -> Unit,
isDarkTheme: Boolean,
@@ -480,6 +499,15 @@ private fun MediaGrid(
horizontalArrangement = 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 = mediaItems,
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
private fun MediaGridItem(
item: MediaItem,

View File

@@ -7,7 +7,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.graphics.Shape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -44,6 +46,7 @@ import kotlinx.coroutines.withContext
* @param onClick Обработчик клика (опционально)
* @param showOnlineIndicator Показывать индикатор онлайн
* @param isOnline Пользователь онлайн
* @param shape Форма аватара (по умолчанию круг)
*/
@Composable
fun AvatarImage(
@@ -53,7 +56,8 @@ fun AvatarImage(
isDarkTheme: Boolean,
onClick: (() -> Unit)? = null,
showOnlineIndicator: Boolean = false,
isOnline: Boolean = false
isOnline: Boolean = false,
shape: Shape = CircleShape
) {
// Получаем аватары из репозитория
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
@@ -85,7 +89,7 @@ fun AvatarImage(
Box(
modifier = Modifier
.size(size)
.clip(CircleShape)
.clip(shape)
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
@@ -113,7 +117,8 @@ fun AvatarImage(
AvatarPlaceholder(
publicKey = publicKey,
size = size,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
shape = shape
)
}
@@ -137,7 +142,8 @@ fun AvatarPlaceholder(
publicKey: String,
size: Dp = 40.dp,
isDarkTheme: Boolean,
fontSize: TextUnit? = null
fontSize: TextUnit? = null,
shape: Shape = CircleShape
) {
val avatarColors = getAvatarColor(publicKey, isDarkTheme)
val avatarText = getAvatarText(publicKey)
@@ -145,7 +151,7 @@ fun AvatarPlaceholder(
Box(
modifier = Modifier
.size(size)
.clip(CircleShape)
.clip(shape)
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
) {