diff --git a/amethyst/build.gradle b/amethyst/build.gradle index f0e902af9..c7e5bb8e6 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -299,5 +299,12 @@ dependencies { debugImplementation platform(libs.androidx.compose.bom) debugImplementation libs.androidx.ui.tooling debugImplementation libs.androidx.ui.test.manifest + + def camerax_version = "1.3.4" + implementation "androidx.camera:camera-core:$camerax_version" + implementation "androidx.camera:camera-camera2:$camerax_version" + implementation "androidx.camera:camera-lifecycle:$camerax_version" + implementation "androidx.camera:camera-view:$camerax_version" + implementation "androidx.camera:camera-extensions:$camerax_version" } diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index 8b30f6ee1..dfd0f2df5 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -16,6 +16,9 @@ + + + @@ -121,6 +124,16 @@ + + + + \ No newline at end of file diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index c55c867c9..839486698 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -24,9 +24,12 @@ import android.Manifest import android.graphics.Bitmap import android.net.Uri import android.os.Build +import android.os.Environment import android.util.Log import android.util.Size import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.border @@ -56,6 +59,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons 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.LocationOff import androidx.compose.material.icons.filled.LocationOn @@ -118,6 +122,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.core.content.FileProvider +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage @@ -182,7 +188,11 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import java.io.File import java.lang.Math.round +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @Composable @@ -197,6 +207,7 @@ fun NewPostView( accountViewModel: AccountViewModel, nav: INav, ) { + val lifecycleOwner = LocalLifecycleOwner.current val postViewModel: NewPostViewModel = viewModel() postViewModel.wantsDirectMessage = enableMessageInterface @@ -206,6 +217,9 @@ fun NewPostView( val scope = rememberCoroutineScope() var showRelaysDialog by remember { mutableStateOf(false) } var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } + var showCamera by remember { + mutableStateOf(true) + } LaunchedEffect(key1 = postViewModel.draftTag) { launch(Dispatchers.IO) { @@ -563,7 +577,6 @@ fun NewPostView( @Composable private fun BottomRowActions(postViewModel: NewPostViewModel) { val scrollState = rememberScrollState() - Row( modifier = Modifier @@ -580,6 +593,12 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) { postViewModel.selectImage(it) } + TakePictureButton( + onPictureTaken = { + postViewModel.selectImage(it) + }, + ) + if (postViewModel.canUsePoll) { // These should be hashtag recommendations the user selects in the future. // val hashtag = stringRes(R.string.poll_hashtag) @@ -625,6 +644,64 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) { } } +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun TakePictureButton(onPictureTaken: (Uri) -> Unit) { + var imageUri by remember { mutableStateOf(null) } + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture(), + ) { success -> + if (success) { + imageUri?.let { + onPictureTaken(it) + } + } + } + val context = LocalContext.current + val cameraPermissionState = + rememberPermissionState( + Manifest.permission.CAMERA, + ) + + Box { + IconButton( + modifier = Modifier.align(Alignment.Center), + onClick = { + if (cameraPermissionState.status.isGranted) { + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + File + .createTempFile( + "JPEG_${timeStamp}_", + ".jpg", + storageDir, + ).apply { + imageUri = + FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + this, + ) + } + imageUri?.let { + launcher.launch(it) + } + } 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 private fun PollField(postViewModel: NewPostViewModel) { val optionsList = postViewModel.pollOptions diff --git a/amethyst/src/main/res/xml/file_paths.xml b/amethyst/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..a075ef96b --- /dev/null +++ b/amethyst/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + +