mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
feat: Add VideoRecorderPreparationSheet
This commit is contained in:
parent
e7e7505592
commit
817e9d96d0
@ -1,5 +1,9 @@
|
||||
package app.myzel394.alibi.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import androidx.camera.core.CameraX
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@ -11,6 +15,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -27,6 +32,7 @@ import app.myzel394.alibi.ui.screens.RecorderScreen
|
||||
import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen
|
||||
import app.myzel394.alibi.ui.screens.SettingsScreen
|
||||
import app.myzel394.alibi.ui.screens.WelcomeScreen
|
||||
import app.myzel394.alibi.ui.utils.CameraInfo
|
||||
|
||||
const val SCALE_IN = 1.25f
|
||||
|
||||
|
@ -0,0 +1,103 @@
|
||||
package app.myzel394.alibi.ui.components.AudioRecorder.atoms
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Camera
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.ui.utils.CameraInfo
|
||||
|
||||
@Composable
|
||||
fun CameraSelectionButton(
|
||||
cameraID: CameraInfo.Lens,
|
||||
selected: Boolean,
|
||||
onSelected: () -> Unit,
|
||||
label: String,
|
||||
description: String? = null,
|
||||
) {
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = if (selected) MaterialTheme.colorScheme.secondaryContainer.copy(
|
||||
alpha = 0.2f
|
||||
) else Color.Transparent,
|
||||
// Make animation about 0.5x faster than default
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||
),
|
||||
label = "backgroundColor"
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable(onClick = onSelected)
|
||||
.background(backgroundColor)
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = onSelected,
|
||||
)
|
||||
if (description == null) {
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
Text(
|
||||
description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
CAMERA_LENS_ICON_MAP[cameraID]!!,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val CAMERA_LENS_ICON_MAP = mapOf(
|
||||
CameraInfo.Lens.BACK to Icons.Default.Camera,
|
||||
CameraInfo.Lens.FRONT to Icons.Default.Person,
|
||||
CameraInfo.Lens.EXTERNAL to Icons.Default.Videocam,
|
||||
)
|
@ -1,11 +0,0 @@
|
||||
package app.myzel394.alibi.ui.components.AudioRecorder.atoms
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VideoRecorderPreparationSheet() {
|
||||
val sheetState = rememberModalBottomSheetState()
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Camera
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.ui.components.AudioRecorder.atoms.CameraSelectionButton
|
||||
import app.myzel394.alibi.ui.models.VideoRecorderSettingsModel
|
||||
import app.myzel394.alibi.ui.utils.CameraInfo
|
||||
|
||||
@Composable
|
||||
fun CamerasSelection(
|
||||
cameras: Iterable<CameraInfo>,
|
||||
videoSettings: VideoRecorderSettingsModel
|
||||
) {
|
||||
val CAMERA_LENS_TEXT_MAP = mapOf(
|
||||
CameraInfo.Lens.BACK to stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_back_label),
|
||||
CameraInfo.Lens.FRONT to stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_front_label),
|
||||
CameraInfo.Lens.EXTERNAL to stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_external_label),
|
||||
)
|
||||
|
||||
Column {
|
||||
if (cameras.count() == 2 && cameras.elementAt(0).id == 0 && cameras.elementAt(1).id == 1) {
|
||||
CameraSelectionButton(
|
||||
cameraID = CameraInfo.Lens.BACK,
|
||||
label = stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_back_label),
|
||||
selected = videoSettings.cameraID == 0,
|
||||
onSelected = {
|
||||
videoSettings.cameraID = 0
|
||||
},
|
||||
)
|
||||
CameraSelectionButton(
|
||||
cameraID = CameraInfo.Lens.FRONT,
|
||||
label = stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_front_label),
|
||||
selected = videoSettings.cameraID == 1,
|
||||
onSelected = {
|
||||
videoSettings.cameraID = 1
|
||||
},
|
||||
)
|
||||
} else {
|
||||
cameras.forEach { camera ->
|
||||
CameraSelectionButton(
|
||||
cameraID = CameraInfo.CAMERA_INT_TO_LENS_MAP[camera.id]!!,
|
||||
selected = videoSettings.cameraID == camera.id,
|
||||
onSelected = {
|
||||
videoSettings.cameraID = camera.id
|
||||
},
|
||||
label = stringResource(
|
||||
R.string.ui_videoRecorder_action_start_settings_cameraLens_label,
|
||||
camera.id
|
||||
),
|
||||
description = CAMERA_LENS_TEXT_MAP[camera.lens]!!,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package app.myzel394.alibi.ui.components.AudioRecorder.molecules
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Camera
|
||||
import androidx.compose.material.icons.filled.CameraAlt
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
|
||||
import app.myzel394.alibi.ui.components.atoms.GlobalSwitch
|
||||
import app.myzel394.alibi.ui.models.VideoRecorderSettingsModel
|
||||
import app.myzel394.alibi.ui.utils.CameraInfo
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VideoRecorderPreparationSheet(
|
||||
onDismiss: () -> Unit,
|
||||
videoSettings: VideoRecorderSettingsModel = viewModel()
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(true)
|
||||
|
||||
val context = LocalContext.current
|
||||
val cameras = CameraInfo.queryAvailableCameras(context)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(30.dp),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CameraAlt,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(80.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.ui_videoRecorder_action_start_settings_label),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
GlobalSwitch(
|
||||
label = stringResource(R.string.ui_videoRecorder_action_start_settings_enableAudio_label),
|
||||
checked = videoSettings.enableAudio,
|
||||
onCheckedChange = {
|
||||
videoSettings.enableAudio = it
|
||||
}
|
||||
)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_selection_label),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
CamerasSelection(
|
||||
cameras = cameras,
|
||||
videoSettings = videoSettings,
|
||||
)
|
||||
|
||||
val label = stringResource(R.string.ui_videoRecorder_action_start_settings_start_label)
|
||||
Button(
|
||||
onClick = {},
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
.height(BIG_PRIMARY_BUTTON_SIZE)
|
||||
.semantics {
|
||||
contentDescription = label
|
||||
}
|
||||
) {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@ -31,7 +30,6 @@ import androidx.compose.ui.unit.dp
|
||||
import app.myzel394.alibi.R
|
||||
import app.myzel394.alibi.db.AppSettings
|
||||
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
|
||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||
import app.myzel394.alibi.ui.models.VideoRecorderModel
|
||||
|
||||
@Composable
|
||||
@ -41,24 +39,24 @@ fun VideoRecordingStart(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// We can't get the current `notificationDetails` inside the
|
||||
// `onPermissionAvailable` function. We'll instead use this hack
|
||||
// with `LaunchedEffect` to get the current value.
|
||||
var startRecording by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(startRecording) {
|
||||
if (startRecording) {
|
||||
startRecording = false
|
||||
var showSheet by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
videoRecorder.startRecording(context, appSettings)
|
||||
}
|
||||
if (showSheet) {
|
||||
VideoRecorderPreparationSheet(
|
||||
onDismiss = {
|
||||
showSheet = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
PermissionRequester(
|
||||
permission = Manifest.permission.RECORD_AUDIO,
|
||||
icon = Icons.Default.Mic,
|
||||
onPermissionAvailable = {
|
||||
startRecording = true
|
||||
}
|
||||
showSheet = true
|
||||
},
|
||||
) { trigger ->
|
||||
val label = stringResource(R.string.ui_videoRecorder_action_start_label)
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
package app.myzel394.alibi.ui.models
|
||||
|
||||
import android.graphics.Camera
|
||||
import android.hardware.camera2.CameraManager
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class VideoRecorderSettingsModel : ViewModel() {
|
||||
var enableAudio by mutableStateOf(true)
|
||||
var cameraID by mutableIntStateOf(0)
|
||||
}
|
@ -1,2 +1,44 @@
|
||||
package app.myzel394.alibi.ui.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.hardware.camera2.CameraMetadata
|
||||
|
||||
data class CameraInfo(
|
||||
val lens: Lens,
|
||||
val id: Int,
|
||||
) {
|
||||
enum class Lens(val androidValue: Int) {
|
||||
BACK(CameraMetadata.LENS_FACING_BACK),
|
||||
FRONT(CameraMetadata.LENS_FACING_FRONT),
|
||||
EXTERNAL(CameraMetadata.LENS_FACING_EXTERNAL),
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CAMERA_INT_TO_LENS_MAP = mapOf(
|
||||
0 to Lens.BACK,
|
||||
1 to Lens.FRONT,
|
||||
2 to Lens.EXTERNAL,
|
||||
)
|
||||
|
||||
fun queryAvailableCameras(context: Context): List<CameraInfo> {
|
||||
val camera = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
|
||||
return camera.cameraIdList.map { id ->
|
||||
val lensFacing =
|
||||
camera.getCameraCharacteristics(id).get(CameraCharacteristics.LENS_FACING)
|
||||
?: return@map null
|
||||
|
||||
fromCameraId(id, lensFacing)
|
||||
}.filterNotNull()
|
||||
}
|
||||
|
||||
fun fromCameraId(cameraId: String, lensFacing: Int): CameraInfo {
|
||||
return CameraInfo(
|
||||
lens = CAMERA_INT_TO_LENS_MAP[lensFacing]!!,
|
||||
id = cameraId.toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -139,4 +139,12 @@
|
||||
<string name="ui_settings_value_videoQuality_values_sd">Standard</string>
|
||||
<string name="ui_settings_value_videoQuality_values_lowest">Lowest</string>
|
||||
<string name="ui_videoRecorder_action_start_label">Start Video Recording</string>
|
||||
<string name="ui_videoRecorder_action_start_settings_enableAudio_label">Record with Audio</string>
|
||||
<string name="ui_videoRecorder_action_start_settings_cameraLens_back_label">Back facing</string>
|
||||
<string name="ui_videoRecorder_action_start_settings_cameraLens_front_label">Front facing</string>
|
||||
<string name="ui_videoRecorder_action_start_settings_cameraLens_external_label">External Camera</string>
|
||||
<string name="ui_videoRecorder_action_start_settings_cameraLens_label">Camera %s</string>
|
||||
<string name="ui_videoRecorder_action_start_settings_start_label">Start Recording</string>
|
||||
<string name="ui_videoRecorder_action_start_settings_label">Prepare your recording</string>
|
||||
<string name="ui_videoRecorder_action_start_settings_cameraLens_selection_label">Select camera</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user