Merge pull request #80 from Myzel394/fix-71-improve-landscape

Improve landscape
This commit is contained in:
Myzel394 2024-02-23 23:48:32 +01:00 committed by GitHub
commit 38fc299fc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 605 additions and 352 deletions

View File

@ -1,3 +1,9 @@
# services
This folder contains all available services.
## VideoRecorderService
I found it a bit confusing on how to properly handle the services, so I made this diagram
to help me understand it better. I hope it helps you too.

View File

@ -194,7 +194,9 @@ class VideoRecorderService :
selectedCamera,
videoCapture
)
cameraControl = CameraControl(camera!!)
cameraControl = CameraControl(camera!!).also {
it.init()
}
onCameraControlAvailable()
_cameraAvailableListener.complete(Unit)
@ -309,17 +311,25 @@ class VideoRecorderService :
}
class CameraControl(
val camera: Camera
val camera: Camera,
// Save state for optimistic updates
var torchEnabled: Boolean = false,
) {
fun init() {
torchEnabled = camera.cameraInfo.torchState.value == TorchState.ON
}
fun enableTorch() {
torchEnabled = true
camera.cameraControl.enableTorch(true)
}
fun disableTorch() {
torchEnabled = false
camera.cameraControl.enableTorch(false)
}
fun isTorchEnabled(): Boolean {
fun isHardwareTorchReallyEnabled(): Boolean {
return camera.cameraInfo.torchState.value == TorchState.ON
}

View File

@ -4,6 +4,7 @@ import android.os.Build
import androidx.compose.ui.unit.dp
val BIG_PRIMARY_BUTTON_SIZE = 64.dp
val BIG_PRIMARY_BUTTON_MAX_WIDTH = 450.dp
val SHEET_BOTTOM_OFFSET = 24.dp
val MAX_AMPLITUDE = 20000

View File

@ -0,0 +1,14 @@
# ui
This folder contains all user interfaces. The folder is structured as follows:
* `components`: contains all reusable components
* `atoms`, `molecules`, `organisms`, `pages`: contains components that are generic and can be
reused in different contexts
* `<name>Screen/{atoms,molecules,organisms,pages}`: contains components that are specific to a
screen
* `screens`: contains all screens. Screens are composed of components from the `components` folder
* `models`: contains view models used by the screens
* `utils`: contains general utility functions
The root Kotlin files are used for the general setup of the UI.

View File

@ -1,6 +1,6 @@
package app.myzel394.alibi.ui.components.RecorderScreen.atoms
import android.Manifest
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -11,10 +11,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -25,12 +22,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R
import app.myzel394.alibi.ui.utils.PermissionHelper
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -41,8 +36,10 @@ fun BigButton(
onClick: () -> Unit,
onLongClick: () -> Unit = {},
) {
val orientation = LocalConfiguration.current.orientation
BoxWithConstraints {
val isLarge = maxWidth > 500.dp
val isLarge = maxWidth > 500.dp && orientation == Configuration.ORIENTATION_PORTRAIT
Column(
modifier = Modifier

View File

@ -61,11 +61,18 @@ fun RealtimeAudioVisualizer(
}
val configuration = LocalConfiguration.current
val screenWidth = with(LocalDensity.current) { configuration.screenWidthDp.dp.toPx() }
// Use greater value of width and height to make sure the amplitudes are shown
// when the user rotates the device
val availableSpace = with(LocalDensity.current) {
Math.max(
configuration.screenWidthDp.dp.toPx(),
configuration.screenHeightDp.dp.toPx()
)
}
LaunchedEffect(screenWidth) {
LaunchedEffect(availableSpace) {
// Add 1 to allow the visualizer to overflow the screen
audioRecorder.setMaxAmplitudesAmount(ceil(screenWidth.toInt() / BOX_DIFF).toInt() + 1)
audioRecorder.setMaxAmplitudesAmount(ceil(availableSpace.toInt() / BOX_DIFF).toInt() + 1)
}
Canvas(modifier = modifier) {

View File

@ -1,45 +1,22 @@
package app.myzel394.alibi.ui.components.RecorderScreen.molecules
import android.Manifest
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.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
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.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.unit.dp
import app.myzel394.alibi.R
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.BigButton
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.utils.PermissionHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun AudioRecordingStart(
@ -62,7 +39,7 @@ fun AudioRecordingStart(
PermissionRequester(
permission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
icon = Icons.Default.InsertDriveFile,
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
onPermissionAvailable = {
startRecording = true
}

View File

@ -1,8 +1,11 @@
package app.myzel394.alibi.ui.components.RecorderScreen.molecules
import android.content.res.Configuration
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
@ -14,6 +17,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.DeleteButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.PauseResumeButton
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.SaveButton
@ -23,6 +28,8 @@ import kotlinx.coroutines.delay
@Composable
fun RecordingControl(
modifier: Modifier = Modifier,
orientation: Int = LocalConfiguration.current.orientation,
initialDelay: Long = 0L,
isPaused: Boolean,
recordingTime: Long,
@ -85,8 +92,55 @@ fun RecordingControl(
}
}
when (orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.alpha(saveButtonAlpha),
contentAlignment = Alignment.Center,
) {
SaveButton(
onSave = onSave,
modifier = Modifier.fillMaxWidth(),
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.alpha(pauseButtonAlpha),
contentAlignment = Alignment.Center,
) {
PauseResumeButton(
isPaused = isPaused,
onChange = onPauseResume,
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.alpha(deleteButtonAlpha),
contentAlignment = Alignment.Center,
) {
DeleteButton(
onDelete = onDelete,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
else -> {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
Box(
modifier = Modifier
@ -119,4 +173,6 @@ fun RecordingControl(
SaveButton(onSave = onSave)
}
}
}
}
}

View File

@ -40,6 +40,7 @@ fun RecordingStatus(
progress: Float,
recordingStart: LocalDateTime,
maxDuration: Long,
progressModifier: Modifier = Modifier.width(300.dp),
) {
val animateIn = rememberInitialRecordingAnimation(recordingTime)
@ -73,9 +74,8 @@ fun RecordingStatus(
)
) {
LinearProgressIndicator(
progress = progress,
modifier = Modifier
.width(300.dp)
progress = { progress },
modifier = progressModifier,
)
}

View File

@ -2,50 +2,18 @@ package app.myzel394.alibi.ui.components.RecorderScreen.molecules
import android.Manifest
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.pointerInput
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.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import app.myzel394.alibi.R
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.BigButton
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
import app.myzel394.alibi.ui.models.VideoRecorderModel
@ -83,7 +51,7 @@ fun VideoRecordingStart(
PermissionRequester(
permission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
icon = Icons.Default.InsertDriveFile,
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
onPermissionAvailable = {
showSheet = true
}

View File

@ -1,9 +1,12 @@
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.HorizontalDivider
@ -16,6 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.ui.components.RecorderScreen.atoms.RealtimeAudioVisualizer
@ -33,6 +37,7 @@ fun AudioRecordingStatus(
audioRecorder: AudioRecorderModel,
) {
val context = LocalContext.current
val configuration = LocalConfiguration.current.orientation
val scope = rememberCoroutineScope()
@ -62,6 +67,61 @@ fun AudioRecordingStatus(
.weight(1f),
)
when (configuration) {
Configuration.ORIENTATION_LANDSCAPE -> {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Column(
verticalArrangement = Arrangement
.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.weight(3f),
) {
RecordingStatus(
recordingTime = audioRecorder.recordingTime,
progress = audioRecorder.progress,
recordingStart = audioRecorder.recordingStart,
maxDuration = audioRecorder.settings!!.maxDuration,
progressModifier = Modifier.fillMaxWidth(.9f),
)
MicrophoneStatus(audioRecorder)
}
RecordingControl(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
isPaused = audioRecorder.isPaused,
recordingTime = audioRecorder.recordingTime,
onDelete = {
scope.launch {
runCatching {
audioRecorder.stopRecording(context)
}
runCatching {
audioRecorder.destroyService(context)
}
audioRecorder.batchesFolder!!.deleteRecordings()
}
},
onPauseResume = {
if (audioRecorder.isPaused) {
audioRecorder.resumeRecording()
} else {
audioRecorder.pauseRecording()
}
},
onSave = {
audioRecorder.onRecordingSave(false)
}
)
}
}
else -> {
RecordingStatus(
recordingTime = audioRecorder.recordingTime,
progress = audioRecorder.progress,
@ -105,4 +165,6 @@ fun AudioRecordingStatus(
)
}
}
}
}
}

View File

@ -1,5 +1,6 @@
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -9,13 +10,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -29,6 +30,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -40,6 +42,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_MAX_WIDTH
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.AudioRecordingStart
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.QuickMaxDurationSelector
@ -64,6 +67,7 @@ fun StartRecording(
showAudioRecorder: Boolean,
) {
val context = LocalContext.current
val orientation = LocalConfiguration.current.orientation
val label = stringResource(
R.string.ui_recorder_action_start_description_2,
@ -98,10 +102,33 @@ fun StartRecording(
Column(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 32.dp),
.padding(bottom = if (orientation == Configuration.ORIENTATION_PORTRAIT) 32.dp else 16.dp),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
) {
when (orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
if (showAudioRecorder)
AudioRecordingStart(
audioRecorder = audioRecorder,
appSettings = appSettings,
)
VideoRecordingStart(
videoRecorder = videoRecorder,
appSettings = appSettings,
onHideAudioRecording = onHideTopBar,
onShowAudioRecording = onShowTopBar,
showPreview = !showAudioRecorder,
)
}
}
else -> {
Spacer(modifier = Modifier.weight(1f))
if (showAudioRecorder)
@ -116,6 +143,9 @@ fun StartRecording(
onShowAudioRecording = onShowTopBar,
showPreview = !showAudioRecorder,
)
}
}
val forceUpdate = rememberForceUpdateOnLifeCycleChange()
Column(
@ -133,6 +163,7 @@ fun StartRecording(
TextButton(
modifier = Modifier
.fillMaxWidth()
.requiredWidthIn(max = BIG_PRIMARY_BUTTON_MAX_WIDTH)
.height(BIG_PRIMARY_BUTTON_SIZE)
.semantics {
contentDescription = label

View File

@ -1,10 +1,13 @@
package app.myzel394.alibi.ui.components.RecorderScreen.organisms
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -16,9 +19,14 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
@ -37,28 +45,96 @@ import kotlinx.coroutines.launch
fun VideoRecordingStatus(
videoRecorder: VideoRecorderModel,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val availableCameras = CameraInfo.queryAvailableCameras(context)
val orientation = LocalConfiguration.current.orientation
KeepScreenOn()
when (orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement
.spacedBy(32.dp),
modifier = Modifier
.weight(1f)
.fillMaxWidth(0.9f)
.align(Alignment.CenterVertically),
) {
_VideoGeneralInfo(videoRecorder)
_VideoRecordingStatus(videoRecorder)
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(0.9f)
) {
Column(
verticalArrangement = Arrangement
.spacedBy(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
_VideoControls(videoRecorder)
HorizontalDivider()
_PrimitiveControls(videoRecorder)
}
}
}
}
else -> {
Column(
modifier = Modifier
.fillMaxSize(),
.fillMaxSize()
.padding(bottom = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
) {
Box {}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement
.spacedBy(24.dp),
.spacedBy(16.dp),
) {
_VideoGeneralInfo(videoRecorder)
_VideoRecordingStatus(videoRecorder)
}
Column(
verticalArrangement = Arrangement
.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
_VideoControls(videoRecorder)
HorizontalDivider()
_PrimitiveControls(videoRecorder)
}
}
}
}
}
@Composable
fun _VideoGeneralInfo(videoRecorder: VideoRecorderModel) {
val context = LocalContext.current
val availableCameras = CameraInfo.queryAvailableCameras(context)
val orientation = LocalConfiguration.current.orientation
Column(
verticalArrangement = Arrangement
.spacedBy(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 12.dp else 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
Icons.Default.CameraAlt,
contentDescription = null,
modifier = Modifier.size(64.dp)
modifier = Modifier.size(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 48.dp else 64.dp)
)
if (videoRecorder.isStartingRecording) {
@ -98,49 +174,25 @@ fun VideoRecordingStatus(
)
}
}
}
if (videoRecorder.isStartingRecording) {
Text(
stringResource(R.string.ui_videoRecorder_info_starting),
style = MaterialTheme.typography.labelMedium,
)
} else {
@Composable
fun _VideoRecordingStatus(videoRecorder: VideoRecorderModel) {
RecordingStatus(
recordingTime = videoRecorder.recordingTime,
progress = videoRecorder.progress,
recordingStart = videoRecorder.recordingStart,
maxDuration = videoRecorder.settings!!.maxDuration,
)
}
}
Column(
verticalArrangement = Arrangement
.spacedBy(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(bottom = 32.dp),
) {
if (!videoRecorder.isStartingRecording) {
val cameraControl = videoRecorder.recorderService!!.cameraControl!!
if (cameraControl.hasTorchAvailable()) {
val isTorchEnabled = cameraControl.isTorchEnabled()
@Composable
fun _PrimitiveControls(videoRecorder: VideoRecorderModel) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
TorchStatus(
enabled = isTorchEnabled,
onChange = {
if (isTorchEnabled) {
cameraControl.disableTorch()
} else {
cameraControl.enableTorch()
}
},
)
}
}
HorizontalDivider()
if (!videoRecorder.isStartingRecording) {
RecordingControl(
orientation = Configuration.ORIENTATION_PORTRAIT,
// There may be some edge cases where the app may crash if the
// user stops or pauses the recording too soon, so we simply add a
// small delay to prevent that
@ -169,9 +221,27 @@ fun VideoRecordingStatus(
videoRecorder.onRecordingSave(false)
}
)
}
@Composable
fun _VideoControls(videoRecorder: VideoRecorderModel) {
if (!videoRecorder.isStartingRecording) {
val cameraControl = videoRecorder.recorderService!!.cameraControl!!
if (cameraControl.hasTorchAvailable()) {
var torchEnabled by rememberSaveable { mutableStateOf(cameraControl.torchEnabled) }
TorchStatus(
enabled = torchEnabled,
onChange = {
if (torchEnabled) {
torchEnabled = false
cameraControl.disableTorch()
} else {
Box {}
torchEnabled = true
cameraControl.enableTorch()
}
},
)
}
}
}

View File

@ -305,7 +305,9 @@ fun DCIMFolderExplanationDialog(
},
text = {
Column(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(32.dp),
) {
@ -408,7 +410,9 @@ fun InternalFolderExplanationDialog(
},
text = {
Column(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(32.dp),
) {
@ -525,7 +529,8 @@ fun SelectionSheet(
)
if (!SUPPORTS_SAVING_VIDEOS_IN_CUSTOM_FOLDERS) {
Column(
modifier = Modifier.padding(horizontal = 32.dp, vertical = 12.dp),
modifier = Modifier
.padding(horizontal = 32.dp, vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {

View File

@ -1,7 +1,6 @@
package app.myzel394.alibi.ui.components.atoms
import android.app.Activity
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
@ -10,9 +9,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@ -21,14 +20,14 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
@ -173,7 +172,7 @@ fun PermissionRequester(
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
Icons.Default.OpenInNew,
Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize),
)

View File

@ -0,0 +1,39 @@
package app.myzel394.alibi.ui.components.atoms
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Rotate90DegreesCw
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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
@Composable
fun RotateDeviceToPortrait(
modifier: Modifier = Modifier
.fillMaxWidth()
.widthIn(max = 300.dp),
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.Rotate90DegreesCw,
contentDescription = stringResource(R.string.ui_rotateDevice_portrait_label),
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.tertiary,
)
Text(stringResource(R.string.ui_rotateDevice_portrait_label))
}
}

View File

@ -1,6 +1,5 @@
package app.myzel394.alibi.ui.screens
import android.widget.Space
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -17,8 +16,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -42,11 +41,10 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import app.myzel394.alibi.R
import app.myzel394.alibi.BuildConfig
import app.myzel394.alibi.ui.TRANSLATION_HELP_URL
import app.myzel394.alibi.R
import app.myzel394.alibi.ui.REPO_URL
import app.myzel394.alibi.ui.TRANSLATION_HELP_URL
import app.myzel394.alibi.ui.components.AboutScreen.atoms.DonationsTile
import app.myzel394.alibi.ui.components.AboutScreen.atoms.GPGKeyOverview
@ -68,9 +66,10 @@ fun AboutScreen(
},
navigationIcon = {
IconButton(onClick = onBackNavigate) {
val label = stringResource(R.string.goBack)
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back"
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = label,
)
}
},
@ -159,7 +158,7 @@ fun AboutScreen(
fontWeight = FontWeight.Bold,
)
Icon(
Icons.Default.OpenInNew,
Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize)
)
@ -196,7 +195,7 @@ fun AboutScreen(
fontWeight = FontWeight.Bold,
)
Icon(
Icons.Default.OpenInNew,
Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize)
)

View File

@ -1,18 +1,11 @@
package app.myzel394.alibi.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -29,22 +22,20 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
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.unit.dp
import androidx.navigation.NavController
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.NotificationSettings
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.LandingElement
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms.NotificationEditor
import app.myzel394.alibi.ui.components.atoms.RotateDeviceToPortrait
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@ -80,9 +71,10 @@ fun CustomRecordingNotificationsScreen(
},
navigationIcon = {
IconButton(onClick = onBackNavigate) {
val label = stringResource(R.string.goBack)
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back"
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = label,
)
}
},
@ -92,6 +84,10 @@ fun CustomRecordingNotificationsScreen(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
) { padding ->
val orientation = LocalConfiguration.current.orientation
when (orientation) {
Configuration.ORIENTATION_PORTRAIT -> {
if (showEditor) {
val scope = rememberCoroutineScope()
@ -121,4 +117,17 @@ fun CustomRecordingNotificationsScreen(
)
}
}
else -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center,
) {
RotateDeviceToPortrait()
}
}
}
}
}

View File

@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -38,21 +38,21 @@ import app.myzel394.alibi.dataStore
import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AboutTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderEncoderTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.VideoRecorderFrameRateTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderOutputFormatTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderSamplingRateTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderShowAllMicrophonesTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.CustomNotificationTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.DeleteRecordingsImmediatelyTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.DividerTitle
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.VideoRecorderQualityTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.EnableAppLockTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.ImportExport
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.InAppLanguagePicker
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.IntervalDurationTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.MaxDurationTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderOutputFormatTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderSamplingRateTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.SaveFolderTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderShowAllMicrophonesTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.EnableAppLockTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.VideoRecorderBitrateTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.VideoRecorderFrameRateTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.VideoRecorderQualityTile
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.InAppLanguagePicker
import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ThemeSelector
import app.myzel394.alibi.ui.components.atoms.GlobalSwitch
import app.myzel394.alibi.ui.components.atoms.MessageBox
@ -99,9 +99,10 @@ fun SettingsScreen(
},
navigationIcon = {
IconButton(onClick = onBackNavigate) {
val label = stringResource(R.string.goBack)
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back"
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = label,
)
}
},

View File

@ -1,4 +1,4 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Alibi</string>
<string name="dialog_close_cancel_label">Cancel</string>
@ -189,4 +189,6 @@
<string name="ui_settings_option_saveFolder_explainMediaFolder_openFolderExplanation">Tap on a folder to open it in the Files app</string>
<string name="ui_videoRecorder_action_start_settings_cameraLens_unknown_label">Unknown</string>
<string name="ui_settings_option_saveFolder_explainInternalFolder_explanation">To protect your privacy, Alibi stores its batches into its own private, encrypted storage. This storage is only accessible by Alibi and can\'t be accessed by other apps or by a possible intruder. Once you save the recording, you will be asked where you want to save the recording to.</string>
<string name="ui_rotateDevice_portrait_label">Please rotate your device to portait mode</string>
<string name="goBack">Back</string>
</resources>