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 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. to help me understand it better. I hope it helps you too.

View File

@ -194,7 +194,9 @@ class VideoRecorderService :
selectedCamera, selectedCamera,
videoCapture videoCapture
) )
cameraControl = CameraControl(camera!!) cameraControl = CameraControl(camera!!).also {
it.init()
}
onCameraControlAvailable() onCameraControlAvailable()
_cameraAvailableListener.complete(Unit) _cameraAvailableListener.complete(Unit)
@ -309,17 +311,25 @@ class VideoRecorderService :
} }
class CameraControl( 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() { fun enableTorch() {
torchEnabled = true
camera.cameraControl.enableTorch(true) camera.cameraControl.enableTorch(true)
} }
fun disableTorch() { fun disableTorch() {
torchEnabled = false
camera.cameraControl.enableTorch(false) camera.cameraControl.enableTorch(false)
} }
fun isTorchEnabled(): Boolean { fun isHardwareTorchReallyEnabled(): Boolean {
return camera.cameraInfo.torchState.value == TorchState.ON return camera.cameraInfo.torchState.value == TorchState.ON
} }

View File

@ -4,6 +4,7 @@ import android.os.Build
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
val BIG_PRIMARY_BUTTON_SIZE = 64.dp val BIG_PRIMARY_BUTTON_SIZE = 64.dp
val BIG_PRIMARY_BUTTON_MAX_WIDTH = 450.dp
val SHEET_BOTTOM_OFFSET = 24.dp val SHEET_BOTTOM_OFFSET = 24.dp
val MAX_AMPLITUDE = 20000 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 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.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource 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.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.Mic
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -25,12 +22,10 @@ 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.vector.ImageVector 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.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R
import app.myzel394.alibi.ui.utils.PermissionHelper
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@ -41,8 +36,10 @@ fun BigButton(
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit = {}, onLongClick: () -> Unit = {},
) { ) {
val orientation = LocalConfiguration.current.orientation
BoxWithConstraints { BoxWithConstraints {
val isLarge = maxWidth > 500.dp val isLarge = maxWidth > 500.dp && orientation == Configuration.ORIENTATION_PORTRAIT
Column( Column(
modifier = Modifier modifier = Modifier

View File

@ -61,11 +61,18 @@ fun RealtimeAudioVisualizer(
} }
val configuration = LocalConfiguration.current 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 // 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) { Canvas(modifier = modifier) {

View File

@ -1,45 +1,22 @@
package app.myzel394.alibi.ui.components.RecorderScreen.molecules package app.myzel394.alibi.ui.components.RecorderScreen.molecules
import android.Manifest 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.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.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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue 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.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.R
import app.myzel394.alibi.db.AppSettings 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.RecorderScreen.atoms.BigButton
import app.myzel394.alibi.ui.components.atoms.PermissionRequester import app.myzel394.alibi.ui.components.atoms.PermissionRequester
import app.myzel394.alibi.ui.models.AudioRecorderModel 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 @Composable
fun AudioRecordingStart( fun AudioRecordingStart(
@ -62,7 +39,7 @@ fun AudioRecordingStart(
PermissionRequester( PermissionRequester(
permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, permission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
icon = Icons.Default.InsertDriveFile, icon = Icons.AutoMirrored.Filled.InsertDriveFile,
onPermissionAvailable = { onPermissionAvailable = {
startRecording = true startRecording = true
} }

View File

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

View File

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

View File

@ -2,50 +2,18 @@ package app.myzel394.alibi.ui.components.RecorderScreen.molecules
import android.Manifest import android.Manifest
import androidx.compose.foundation.ExperimentalFoundationApi 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.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.CameraAlt 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue 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.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.R
import app.myzel394.alibi.db.AppSettings 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.RecorderScreen.atoms.BigButton
import app.myzel394.alibi.ui.components.atoms.PermissionRequester import app.myzel394.alibi.ui.components.atoms.PermissionRequester
import app.myzel394.alibi.ui.models.VideoRecorderModel import app.myzel394.alibi.ui.models.VideoRecorderModel
@ -83,7 +51,7 @@ fun VideoRecordingStart(
PermissionRequester( PermissionRequester(
permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, permission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
icon = Icons.Default.InsertDriveFile, icon = Icons.AutoMirrored.Filled.InsertDriveFile,
onPermissionAvailable = { onPermissionAvailable = {
showSheet = true showSheet = true
} }

View File

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

View File

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

View File

@ -1,10 +1,13 @@
package app.myzel394.alibi.ui.components.RecorderScreen.organisms package app.myzel394.alibi.ui.components.RecorderScreen.organisms
import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -16,9 +19,14 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -37,141 +45,203 @@ import kotlinx.coroutines.launch
fun VideoRecordingStatus( fun VideoRecordingStatus(
videoRecorder: VideoRecorderModel, videoRecorder: VideoRecorderModel,
) { ) {
val context = LocalContext.current val orientation = LocalConfiguration.current.orientation
val scope = rememberCoroutineScope()
val availableCameras = CameraInfo.queryAvailableCameras(context)
KeepScreenOn() KeepScreenOn()
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
) {
Box {}
Column( when (orientation) {
verticalArrangement = Arrangement Configuration.ORIENTATION_LANDSCAPE -> {
.spacedBy(24.dp), Row(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize(),
) { horizontalArrangement = Arrangement.SpaceEvenly,
Icon( verticalAlignment = Alignment.CenterVertically,
Icons.Default.CameraAlt, ) {
contentDescription = null, Column(
modifier = Modifier.size(64.dp) horizontalAlignment = Alignment.CenterHorizontally,
) verticalArrangement = Arrangement
.spacedBy(32.dp),
if (videoRecorder.isStartingRecording) { modifier = Modifier
.weight(1f)
.fillMaxWidth(0.9f)
.align(Alignment.CenterVertically),
) {
_VideoGeneralInfo(videoRecorder)
_VideoRecordingStatus(videoRecorder)
}
Box( Box(
modifier = Modifier modifier = Modifier
.width(128.dp) .weight(1f)
.height( .fillMaxWidth(0.9f)
with(LocalDensity.current) { ) {
MaterialTheme.typography.labelMedium.fontSize.toDp() Column(
} verticalArrangement = Arrangement
) .spacedBy(32.dp),
.shimmer() horizontalAlignment = Alignment.CenterHorizontally,
.background( ) {
MaterialTheme.colorScheme.surfaceVariant, _VideoControls(videoRecorder)
MaterialTheme.shapes.small HorizontalDivider()
) _PrimitiveControls(videoRecorder)
) }
} else {
Text(
stringResource(
R.string.form_value_selected,
if (CameraInfo.checkHasNormalCameras(availableCameras)) {
videoRecorder.cameraID.let {
if (it == CameraInfo.Lens.BACK.androidValue)
stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_back_label)
else
stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_front_label)
}
} else {
stringResource(
R.string.ui_videoRecorder_action_start_settings_cameraLens_label,
videoRecorder.cameraID
)
}
),
style = MaterialTheme.typography.labelMedium,
)
}
}
if (videoRecorder.isStartingRecording) {
Text(
stringResource(R.string.ui_videoRecorder_info_starting),
style = MaterialTheme.typography.labelMedium,
)
} else {
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()
TorchStatus(
enabled = isTorchEnabled,
onChange = {
if (isTorchEnabled) {
cameraControl.disableTorch()
} else {
cameraControl.enableTorch()
}
},
)
} }
} }
}
HorizontalDivider() else -> {
Column(
if (!videoRecorder.isStartingRecording) { modifier = Modifier
RecordingControl( .fillMaxSize()
// There may be some edge cases where the app may crash if the .padding(bottom = 32.dp),
// user stops or pauses the recording too soon, so we simply add a horizontalAlignment = Alignment.CenterHorizontally,
// small delay to prevent that verticalArrangement = Arrangement.SpaceBetween,
initialDelay = 1000L, ) {
isPaused = videoRecorder.isPaused,
recordingTime = videoRecorder.recordingTime,
onDelete = {
scope.launch {
runCatching {
videoRecorder.stopRecording(context)
}
runCatching {
videoRecorder.destroyService(context)
}
videoRecorder.batchesFolder!!.deleteRecordings()
}
},
onPauseResume = {
if (videoRecorder.isPaused) {
videoRecorder.resumeRecording()
} else {
videoRecorder.pauseRecording()
}
},
onSave = {
videoRecorder.onRecordingSave(false)
}
)
} else {
Box {} Box {}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement
.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(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 48.dp else 64.dp)
)
if (videoRecorder.isStartingRecording) {
Box(
modifier = Modifier
.width(128.dp)
.height(
with(LocalDensity.current) {
MaterialTheme.typography.labelMedium.fontSize.toDp()
}
)
.shimmer()
.background(
MaterialTheme.colorScheme.surfaceVariant,
MaterialTheme.shapes.small
)
)
} else {
Text(
stringResource(
R.string.form_value_selected,
if (CameraInfo.checkHasNormalCameras(availableCameras)) {
videoRecorder.cameraID.let {
if (it == CameraInfo.Lens.BACK.androidValue)
stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_back_label)
else
stringResource(R.string.ui_videoRecorder_action_start_settings_cameraLens_front_label)
}
} else {
stringResource(
R.string.ui_videoRecorder_action_start_settings_cameraLens_label,
videoRecorder.cameraID
)
}
),
style = MaterialTheme.typography.labelMedium,
)
}
}
}
@Composable
fun _VideoRecordingStatus(videoRecorder: VideoRecorderModel) {
RecordingStatus(
recordingTime = videoRecorder.recordingTime,
progress = videoRecorder.progress,
recordingStart = videoRecorder.recordingStart,
maxDuration = videoRecorder.settings!!.maxDuration,
)
}
@Composable
fun _PrimitiveControls(videoRecorder: VideoRecorderModel) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
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
initialDelay = 1000L,
isPaused = videoRecorder.isPaused,
recordingTime = videoRecorder.recordingTime,
onDelete = {
scope.launch {
runCatching {
videoRecorder.stopRecording(context)
}
runCatching {
videoRecorder.destroyService(context)
}
videoRecorder.batchesFolder!!.deleteRecordings()
}
},
onPauseResume = {
if (videoRecorder.isPaused) {
videoRecorder.resumeRecording()
} else {
videoRecorder.pauseRecording()
}
},
onSave = {
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 {
torchEnabled = true
cameraControl.enableTorch()
}
},
)
}
}
} }

View File

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

View File

@ -1,7 +1,6 @@
package app.myzel394.alibi.ui.components.atoms package app.myzel394.alibi.ui.components.atoms
import android.app.Activity import android.app.Activity
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column 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.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons 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.Cancel
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@ -21,14 +20,14 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
@ -173,7 +172,7 @@ fun PermissionRequester(
contentPadding = ButtonDefaults.ButtonWithIconContentPadding, contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) { ) {
Icon( Icon(
Icons.Default.OpenInNew, Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize), 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 package app.myzel394.alibi.ui.screens
import android.widget.Space
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -17,8 +16,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
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.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon 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.semantics.semantics
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import app.myzel394.alibi.R
import app.myzel394.alibi.BuildConfig 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.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.DonationsTile
import app.myzel394.alibi.ui.components.AboutScreen.atoms.GPGKeyOverview import app.myzel394.alibi.ui.components.AboutScreen.atoms.GPGKeyOverview
@ -68,9 +66,10 @@ fun AboutScreen(
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackNavigate) { IconButton(onClick = onBackNavigate) {
val label = stringResource(R.string.goBack)
Icon( Icon(
Icons.Default.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back" contentDescription = label,
) )
} }
}, },
@ -159,7 +158,7 @@ fun AboutScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
Icon( Icon(
Icons.Default.OpenInNew, Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize) modifier = Modifier.size(ButtonDefaults.IconSize)
) )
@ -196,7 +195,7 @@ fun AboutScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
Icon( Icon(
Icons.Default.OpenInNew, Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize) modifier = Modifier.size(ButtonDefaults.IconSize)
) )

View File

@ -1,18 +1,11 @@
package app.myzel394.alibi.ui.screens package app.myzel394.alibi.ui.screens
import androidx.compose.foundation.clickable import android.content.res.Configuration
import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -29,22 +22,20 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.compose.ui.unit.dp
import androidx.navigation.NavController
import app.myzel394.alibi.R import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.NotificationSettings import app.myzel394.alibi.db.NotificationSettings
import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.LandingElement 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.CustomRecordingNotificationsScreen.organisms.NotificationEditor
import app.myzel394.alibi.ui.components.atoms.RotateDeviceToPortrait
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -80,9 +71,10 @@ fun CustomRecordingNotificationsScreen(
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackNavigate) { IconButton(onClick = onBackNavigate) {
val label = stringResource(R.string.goBack)
Icon( Icon(
Icons.Default.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back" contentDescription = label,
) )
} }
}, },
@ -92,33 +84,50 @@ fun CustomRecordingNotificationsScreen(
modifier = Modifier modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
) { padding -> ) { padding ->
if (showEditor) { val orientation = LocalConfiguration.current.orientation
val scope = rememberCoroutineScope()
NotificationEditor( when (orientation) {
modifier = Modifier Configuration.ORIENTATION_PORTRAIT -> {
.padding(padding) if (showEditor) {
.padding(vertical = 16.dp), val scope = rememberCoroutineScope()
onNotificationChange = { notificationSettings ->
scope.launch { NotificationEditor(
dataStore.updateData { settings -> modifier = Modifier
settings.setNotificationSettings(notificationSettings.let { .padding(padding)
if (it.preset == NotificationSettings.Preset.Default) .padding(vertical = 16.dp),
null onNotificationChange = { notificationSettings ->
else scope.launch {
it dataStore.updateData { settings ->
}) settings.setNotificationSettings(notificationSettings.let {
if (it.preset == NotificationSettings.Preset.Default)
null
else
it
})
}
}
onBackNavigate()
} }
} )
onBackNavigate() } else {
LandingElement(
onOpenEditor = {
showEditor = true
}
)
} }
) }
} else {
LandingElement( else -> {
onOpenEditor = { Box(
showEditor = true 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.rememberScrollState
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.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon 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.SUPPORTS_DARK_MODE_NATIVELY
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AboutTile 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.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.CustomNotificationTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.DeleteRecordingsImmediatelyTile 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.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.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.IntervalDurationTile
import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.MaxDurationTile 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.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.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.SettingsScreen.atoms.ThemeSelector
import app.myzel394.alibi.ui.components.atoms.GlobalSwitch import app.myzel394.alibi.ui.components.atoms.GlobalSwitch
import app.myzel394.alibi.ui.components.atoms.MessageBox import app.myzel394.alibi.ui.components.atoms.MessageBox
@ -99,9 +99,10 @@ fun SettingsScreen(
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBackNavigate) { IconButton(onClick = onBackNavigate) {
val label = stringResource(R.string.goBack)
Icon( Icon(
Icons.Default.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back" 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="app_name">Alibi</string>
<string name="dialog_close_cancel_label">Cancel</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_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_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_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> </resources>