diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
index b59c132..1bc97bf 100644
--- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
+++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
@@ -419,6 +419,15 @@ data class VideoRecorderSettings(
50 * 1000 * 1000,
100 * 1000 * 1000,
)
+
+ val EXAMPLE_FRAME_RATE_VALUES = listOf(
+ null,
+ 24,
+ 30,
+ 60,
+ 120,
+ 240,
+ )
}
}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/VideoFrameRate.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/VideoFrameRate.kt
new file mode 100644
index 0000000..1ae5cda
--- /dev/null
+++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/Tiles/VideoFrameRate.kt
@@ -0,0 +1,127 @@
+package app.myzel394.alibi.ui.components.SettingsScreen.Tiles
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BrokenImage
+import androidx.compose.material.icons.filled.Tune
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import app.myzel394.alibi.R
+import app.myzel394.alibi.dataStore
+import app.myzel394.alibi.db.AppSettings
+import app.myzel394.alibi.db.VideoRecorderSettings
+import app.myzel394.alibi.ui.components.atoms.ExampleListRoulette
+import app.myzel394.alibi.ui.components.atoms.SettingsTile
+import app.myzel394.alibi.ui.utils.IconResource
+import com.maxkeppeker.sheets.core.models.base.Header
+import com.maxkeppeker.sheets.core.models.base.IconSource
+import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
+import com.maxkeppeler.sheets.input.InputDialog
+import com.maxkeppeler.sheets.input.models.InputHeader
+import com.maxkeppeler.sheets.input.models.InputSelection
+import com.maxkeppeler.sheets.input.models.InputTextField
+import com.maxkeppeler.sheets.input.models.InputTextFieldType
+import com.maxkeppeler.sheets.input.models.ValidationResult
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun VideoFrameRate(
+ settings: AppSettings,
+) {
+ val scope = rememberCoroutineScope()
+ val showDialog = rememberUseCaseState()
+ val dataStore = LocalContext.current.dataStore
+
+ fun updateValue(frameRate: Int?) {
+ scope.launch {
+ dataStore.updateData {
+ it.setVideoRecorderSettings(
+ it.videoRecorderSettings.setTargetFrameRate(frameRate)
+ )
+ }
+ }
+ }
+
+ val notNumberLabel = stringResource(R.string.form_error_type_notNumber)
+ InputDialog(
+ state = showDialog,
+ header = Header.Default(
+ title = stringResource(R.string.ui_settings_option_videoTargetedFrameRate_title),
+ icon = IconSource(
+ painter = IconResource.fromImageVector(Icons.Default.Tune).asPainterResource(),
+ contentDescription = null,
+ )
+ ),
+ selection = InputSelection(
+ input = listOf(
+ InputTextField(
+ header = InputHeader(
+ title = stringResource(id = R.string.ui_settings_option_videoTargetedFrameRate_explanation),
+ ),
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ ),
+ type = InputTextFieldType.OUTLINED,
+ text = if (settings.videoRecorderSettings.targetFrameRate == null) "" else settings.videoRecorderSettings.targetFrameRate.toString(),
+ validationListener = { text ->
+ val frameRate = text?.toIntOrNull()
+
+ if (frameRate == null) {
+ return@InputTextField ValidationResult.Invalid(notNumberLabel)
+ }
+
+ ValidationResult.Valid
+ },
+ key = "framerate",
+ )
+ ),
+ ) { result ->
+ val frameRate = result.getString("framerate")?.toIntOrNull() ?: return@InputSelection
+
+ updateValue(frameRate)
+ }
+ )
+ SettingsTile(
+ title = stringResource(R.string.ui_settings_option_videoTargetedFrameRate_title),
+ leading = {
+ Icon(
+ Icons.Default.BrokenImage,
+ contentDescription = null,
+ )
+ },
+ trailing = {
+ Button(
+ onClick = showDialog::show,
+ colors = ButtonDefaults.filledTonalButtonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant,
+ ),
+ shape = MaterialTheme.shapes.medium,
+ ) {
+ if (settings.videoRecorderSettings.targetFrameRate == null)
+ Text(stringResource(R.string.ui_settings_value_auto_label))
+ else
+ Text(settings.videoRecorderSettings.targetFrameRate.toString())
+ }
+ },
+ extra = {
+ ExampleListRoulette(
+ items = VideoRecorderSettings.EXAMPLE_FRAME_RATE_VALUES,
+ onItemSelected = ::updateValue,
+ ) { frameRate ->
+ Text(
+ frameRate?.toString() ?: stringResource(R.string.ui_settings_value_auto_label)
+ )
+ }
+ }
+ )
+}
diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt
index a208a48..0248ce1 100644
--- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt
+++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt
@@ -38,7 +38,7 @@ import app.myzel394.alibi.R
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.VideoRecorderBitrateTile
+import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.VideoFrameRate
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
@@ -51,6 +51,7 @@ import app.myzel394.alibi.ui.components.SettingsScreen.Tiles.AudioRecorderOutput
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.VideoRecorderBitrateTile
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
@@ -163,7 +164,7 @@ fun SettingsScreen(
description = stringResource(R.string.ui_settings_sections_audio_description),
)
AudioRecorderShowAllMicrophonesTile(settings = settings)
- VideoRecorderBitrateTile(settings = settings)
+ VideoFrameRate(settings = settings)
AudioRecorderSamplingRateTile(settings = settings)
AudioRecorderEncoderTile(
snackbarHostState = snackbarHostState,
@@ -176,6 +177,7 @@ fun SettingsScreen(
description = stringResource(R.string.ui_settings_sections_video_description),
)
VideoRecorderBitrateTile(settings = settings)
+ VideoFrameRate(settings = settings)
}
Divider(
modifier = Modifier
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ee6bbe6..bc51404 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -128,4 +128,7 @@
Bitrate for the video recording. Only applies to the video itself, not the audio. The actual bitrate may be different depending on what you will be recording.
%s MB/s
%s B/s
+ Targeted Frame Rate
+ How many frames per second should be recorded.
+ The actual frame rate may be different. This can for example happen if the device is not able to record with the specified frame rate.
\ No newline at end of file