Merge pull request #1043 from greenart7c3/camera

Take pictures with camera
This commit is contained in:
Vitor Pamplona
2024-09-10 15:32:01 -04:00
committed by GitHub
7 changed files with 239 additions and 17 deletions
+6
View File
@@ -302,5 +302,11 @@ dependencies {
debugImplementation platform(libs.androidx.compose.bom) debugImplementation platform(libs.androidx.compose.bom)
debugImplementation libs.androidx.ui.tooling debugImplementation libs.androidx.ui.tooling
debugImplementation libs.androidx.ui.test.manifest debugImplementation libs.androidx.ui.test.manifest
implementation libs.androidx.camera.core
implementation libs.androidx.camera.camera2
implementation libs.androidx.camera.lifecycle
implementation libs.androidx.camera.view
implementation libs.androidx.camera.extensions
} }
+13
View File
@@ -16,6 +16,9 @@
<!-- To connect with relays --> <!-- To connect with relays -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- To take pictures -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- To Upload media (newer devices) --> <!-- To Upload media (newer devices) -->
<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_VIDEO"/>
@@ -121,6 +124,16 @@
</intent-filter> </intent-filter>
</service> </service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>
@@ -27,6 +27,8 @@ import android.os.Build
import android.util.Log import android.util.Log
import android.util.Size import android.util.Size
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.border import androidx.compose.foundation.border
@@ -56,6 +58,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.material.icons.filled.CurrencyBitcoin
import androidx.compose.material.icons.filled.LocationOff import androidx.compose.material.icons.filled.LocationOff
import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.LocationOn
@@ -118,6 +121,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
@@ -197,6 +201,7 @@ fun NewPostView(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val lifecycleOwner = LocalLifecycleOwner.current
val postViewModel: NewPostViewModel = viewModel() val postViewModel: NewPostViewModel = viewModel()
postViewModel.wantsDirectMessage = enableMessageInterface postViewModel.wantsDirectMessage = enableMessageInterface
@@ -206,6 +211,9 @@ fun NewPostView(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var showRelaysDialog by remember { mutableStateOf(false) } var showRelaysDialog by remember { mutableStateOf(false) }
var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() }
var showCamera by remember {
mutableStateOf(true)
}
LaunchedEffect(key1 = postViewModel.draftTag) { LaunchedEffect(key1 = postViewModel.draftTag) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
@@ -563,7 +571,6 @@ fun NewPostView(
@Composable @Composable
private fun BottomRowActions(postViewModel: NewPostViewModel) { private fun BottomRowActions(postViewModel: NewPostViewModel) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Row( Row(
modifier = modifier =
Modifier Modifier
@@ -580,6 +587,12 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
postViewModel.selectImage(it) postViewModel.selectImage(it)
} }
TakePictureButton(
onPictureTaken = {
postViewModel.selectImage(it)
},
)
if (postViewModel.canUsePoll) { if (postViewModel.canUsePoll) {
// These should be hashtag recommendations the user selects in the future. // These should be hashtag recommendations the user selects in the future.
// val hashtag = stringRes(R.string.poll_hashtag) // val hashtag = stringRes(R.string.poll_hashtag)
@@ -625,6 +638,59 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) {
} }
} }
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun TakePictureButton(onPictureTaken: (Uri) -> Unit) {
var imageUri by remember { mutableStateOf<Uri?>(null) }
val scope = rememberCoroutineScope()
val launcher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture(),
) { success ->
if (success) {
imageUri?.let {
onPictureTaken(it)
}
}
}
val context = LocalContext.current
val cameraPermissionState =
rememberPermissionState(
Manifest.permission.CAMERA,
onPermissionResult = {
if (it) {
scope.launch(Dispatchers.IO) {
imageUri = getPhotoUri(context)
imageUri?.let { uri -> launcher.launch(uri) }
}
}
},
)
Box {
IconButton(
modifier = Modifier.align(Alignment.Center),
onClick = {
if (cameraPermissionState.status.isGranted) {
scope.launch(Dispatchers.IO) {
imageUri = getPhotoUri(context)
imageUri?.let { uri -> launcher.launch(uri) }
}
} else {
cameraPermissionState.launchPermissionRequest()
}
},
) {
Icon(
imageVector = Icons.Default.CameraAlt,
contentDescription = stringRes(id = R.string.upload_image),
modifier = Modifier.height(25.dp),
tint = MaterialTheme.colorScheme.onBackground,
)
}
}
}
@Composable @Composable
private fun PollField(postViewModel: NewPostViewModel) { private fun PollField(postViewModel: NewPostViewModel) {
val optionsList = postViewModel.pollOptions val optionsList = postViewModel.pollOptions
@@ -20,8 +20,10 @@
*/ */
package com.vitorpamplona.amethyst.ui.actions package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
@@ -52,6 +54,7 @@ import androidx.compose.ui.graphics.Brush
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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@@ -60,6 +63,10 @@ import com.vitorpamplona.amethyst.ui.GetMediaActivityResultContract
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@@ -125,6 +132,23 @@ private fun UploadBoxButton(
} }
} }
fun getPhotoUri(context: Context): Uri {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File
.createTempFile(
"JPEG_${timeStamp}_",
".jpg",
storageDir,
).let {
FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
it,
)
}
}
val DefaultAnimationColors = val DefaultAnimationColors =
listOf( listOf(
Color(0xFF5851D8), Color(0xFF5851D8),
@@ -23,11 +23,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video
import android.Manifest import android.Manifest
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddPhotoAlternate
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -45,6 +53,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
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
@@ -57,6 +66,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.GallerySelect import com.vitorpamplona.amethyst.ui.actions.GallerySelect
import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
import com.vitorpamplona.amethyst.ui.actions.NewMediaView import com.vitorpamplona.amethyst.ui.actions.NewMediaView
import com.vitorpamplona.amethyst.ui.actions.getPhotoUri
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@@ -73,7 +83,15 @@ fun NewImageButton(
nav: INav, nav: INav,
navScrollToTop: () -> Unit, navScrollToTop: () -> Unit,
) { ) {
var wantsToPost by remember { mutableStateOf(false) } val context = LocalContext.current
var isOpen by remember { mutableStateOf(false) }
var wantsToPostFromGallery by remember { mutableStateOf(false) }
var wantsToPostFromCamera by remember { mutableStateOf(false) }
var cameraUri by remember { mutableStateOf<Uri?>(null) }
var pickedURI by remember { mutableStateOf<Uri?>(null) } var pickedURI by remember { mutableStateOf<Uri?>(null) }
@@ -87,7 +105,46 @@ fun NewImageButton(
} }
} }
if (wantsToPost) { if (wantsToPostFromCamera) {
val launcher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture(),
) { success ->
if (success) {
cameraUri?.let {
pickedURI = it
}
}
cameraUri = null
wantsToPostFromCamera = false
}
val cameraPermissionState =
rememberPermissionState(
Manifest.permission.CAMERA,
onPermissionResult = {
if (it) {
scope.launch(Dispatchers.IO) {
cameraUri = getPhotoUri(context)
cameraUri?.let { launcher.launch(it) }
}
}
},
)
if (cameraPermissionState.status.isGranted) {
LaunchedEffect(key1 = accountViewModel) {
launch(Dispatchers.IO) {
cameraUri = getPhotoUri(context)
cameraUri?.let { launcher.launch(it) }
}
}
} else {
LaunchedEffect(key1 = accountViewModel) { cameraPermissionState.launchPermissionRequest() }
}
}
if (wantsToPostFromGallery) {
val cameraPermissionState = val cameraPermissionState =
rememberPermissionState( rememberPermissionState(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -102,7 +159,7 @@ fun NewImageButton(
if (showGallerySelect) { if (showGallerySelect) {
GallerySelect( GallerySelect(
onImageUri = { uri -> onImageUri = { uri ->
wantsToPost = false wantsToPostFromGallery = false
showGallerySelect = false showGallerySelect = false
pickedURI = uri pickedURI = uri
}, },
@@ -128,18 +185,62 @@ fun NewImageButton(
if (postViewModel.isUploadingImage) { if (postViewModel.isUploadingImage) {
ShowProgress(postViewModel) ShowProgress(postViewModel)
} else { } else {
FloatingActionButton( Column {
onClick = { wantsToPost = true }, if (isOpen) {
modifier = Size55Modifier, FloatingActionButton(
shape = CircleShape, onClick = {
containerColor = MaterialTheme.colorScheme.primary, wantsToPostFromCamera = true
) { isOpen = false
Icon( },
painter = painterResource(R.drawable.ic_compose), modifier = Size55Modifier,
contentDescription = stringRes(id = R.string.new_short), shape = CircleShape,
modifier = Modifier.size(26.dp), containerColor = MaterialTheme.colorScheme.primary,
tint = Color.White, ) {
) Icon(
imageVector = Icons.Default.CameraAlt,
contentDescription = stringRes(id = R.string.upload_image),
modifier = Modifier.size(26.dp),
tint = Color.White,
)
}
Spacer(modifier = Modifier.height(20.dp))
FloatingActionButton(
onClick = {
wantsToPostFromGallery = true
isOpen = false
},
modifier = Size55Modifier,
shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(
imageVector = Icons.Default.AddPhotoAlternate,
contentDescription = stringRes(id = R.string.upload_image),
modifier = Modifier.size(26.dp),
tint = Color.White,
)
}
Spacer(modifier = Modifier.height(20.dp))
}
FloatingActionButton(
onClick = {
isOpen = !isOpen
},
modifier = Size55Modifier,
shape = CircleShape,
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
contentDescription = stringRes(id = R.string.new_short),
modifier = Modifier.size(26.dp),
tint = Color.White,
)
}
} }
} }
} }
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_files"
path="." />
</paths>
+7 -1
View File
@@ -52,6 +52,7 @@ zoomable = "1.6.2"
zxing = "3.5.3" zxing = "3.5.3"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
windowCoreAndroid = "1.3.0" windowCoreAndroid = "1.3.0"
androidxCamera = "1.3.4"
[libraries] [libraries]
abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" } abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
@@ -61,6 +62,11 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmarkJunit4" } androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmarkJunit4" }
androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric-ktx", version.ref = "biometricKtx" } androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric-ktx", version.ref = "biometricKtx" }
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidxCamera" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidxCamera" }
androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "androidxCamera" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidxCamera" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidxCamera" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -136,4 +142,4 @@ googleServices = { id = "com.google.gms.google-services", version.ref = "gms" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref = 'kotlinxSerializationPlugin' } serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref = 'kotlinxSerializationPlugin' }