feature: add auto-pause playback feature
This commit is contained in:
+33
-2
@@ -15,7 +15,10 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import bzh.ajaury.chombev.player.ui.components.LabeledSlider
|
||||
import bzh.ajaury.chombev.preferences.model.PlaybackPreferences
|
||||
import bzh.ajaury.chombev.preferences.ui.PreferenceToggle
|
||||
import bzh.ajaury.chombev.resources.generated.resources.Res
|
||||
import bzh.ajaury.chombev.resources.generated.resources.auto_pause_description
|
||||
import bzh.ajaury.chombev.resources.generated.resources.auto_pause_title
|
||||
import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_title
|
||||
import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_value
|
||||
import bzh.ajaury.chombev.resources.generated.resources.playback_speed_title
|
||||
@@ -23,16 +26,19 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Bottom sheet letting the user adjust playback: the audio [speed] and the [breakSeconds] silent
|
||||
* pause inserted between phrases.
|
||||
* Bottom sheet letting the user adjust playback: the audio [speed], whether playback stops after
|
||||
* each phrase ([autoPause]), and the [breakSeconds] silent pause inserted between phrases. When
|
||||
* [autoPause] is on, the break setting is ignored and shown as disabled.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PlaybackSpeedBottomSheet(
|
||||
speed: Float,
|
||||
breakSeconds: Float,
|
||||
autoPause: Boolean,
|
||||
onSpeedChange: (Float) -> Unit = {},
|
||||
onBreakChange: (Float) -> Unit = {},
|
||||
onAutoPauseChange: (Boolean) -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
) {
|
||||
@@ -43,8 +49,10 @@ fun PlaybackSpeedBottomSheet(
|
||||
PlaybackSpeedContent(
|
||||
speed = speed,
|
||||
breakSeconds = breakSeconds,
|
||||
autoPause = autoPause,
|
||||
onSpeedChange = onSpeedChange,
|
||||
onBreakChange = onBreakChange,
|
||||
onAutoPauseChange = onAutoPauseChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -53,8 +61,10 @@ fun PlaybackSpeedBottomSheet(
|
||||
private fun PlaybackSpeedContent(
|
||||
speed: Float,
|
||||
breakSeconds: Float,
|
||||
autoPause: Boolean,
|
||||
onSpeedChange: (Float) -> Unit = {},
|
||||
onBreakChange: (Float) -> Unit = {},
|
||||
onAutoPauseChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
// Number of discrete stops strictly between the endpoints (e.g. 0.5..1.0 by 0.1 → 4).
|
||||
val speedSteps = with(PlaybackPreferences) {
|
||||
@@ -79,6 +89,12 @@ private fun PlaybackSpeedContent(
|
||||
steps = speedSteps,
|
||||
onValueChange = onSpeedChange,
|
||||
)
|
||||
PreferenceToggle(
|
||||
title = stringResource(Res.string.auto_pause_title),
|
||||
subtitle = stringResource(Res.string.auto_pause_description),
|
||||
checked = autoPause,
|
||||
onCheckedChange = onAutoPauseChange,
|
||||
)
|
||||
LabeledSlider(
|
||||
title = stringResource(Res.string.break_between_phrases_title),
|
||||
valueLabel = stringResource(
|
||||
@@ -89,6 +105,8 @@ private fun PlaybackSpeedContent(
|
||||
valueRange = 0f..maxBreakSeconds.toFloat(),
|
||||
steps = breakSteps,
|
||||
onValueChange = onBreakChange,
|
||||
// Auto-pause supersedes the break between phrases, so disable it while on.
|
||||
enabled = !autoPause,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -106,6 +124,19 @@ private fun PlaybackSpeedContentPreview() {
|
||||
PlaybackSpeedContent(
|
||||
speed = 0.7f,
|
||||
breakSeconds = 2f,
|
||||
autoPause = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PlaybackSpeedContentAutoPausePreview() {
|
||||
MaterialTheme {
|
||||
PlaybackSpeedContent(
|
||||
speed = 0.7f,
|
||||
breakSeconds = 2f,
|
||||
autoPause = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ fun PlayerScreen(
|
||||
onTextSizeIncrease = viewModel::increaseSubtitleTextSize,
|
||||
onSpeedChange = viewModel::setPlaybackSpeed,
|
||||
onBreakChange = viewModel::setBreakBetweenPhrases,
|
||||
onAutoPauseChange = viewModel::setAutoPause,
|
||||
onBackClicked = onBackClicked,
|
||||
)
|
||||
}
|
||||
@@ -102,6 +103,7 @@ fun PlayerScreen(
|
||||
onTextSizeIncrease: () -> Unit = {},
|
||||
onSpeedChange: (Float) -> Unit = {},
|
||||
onBreakChange: (Float) -> Unit = {},
|
||||
onAutoPauseChange: (Boolean) -> Unit = {},
|
||||
onBackClicked: () -> Unit = {},
|
||||
) {
|
||||
var showPreferences by remember { mutableStateOf(false) }
|
||||
@@ -166,8 +168,10 @@ fun PlayerScreen(
|
||||
PlaybackSpeedBottomSheet(
|
||||
speed = uiState.playbackSpeed,
|
||||
breakSeconds = uiState.breakBetweenPhrases.inWholeSeconds.toFloat(),
|
||||
autoPause = uiState.autoPause,
|
||||
onSpeedChange = onSpeedChange,
|
||||
onBreakChange = onBreakChange,
|
||||
onAutoPauseChange = onAutoPauseChange,
|
||||
onDismiss = { showSpeed = false },
|
||||
)
|
||||
}
|
||||
|
||||
+22
-1
@@ -21,7 +21,11 @@ fun LabeledSlider(
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
steps: Int,
|
||||
onValueChange: (Float) -> Unit = {},
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
// Dim the labels to match the Slider's disabled appearance.
|
||||
val contentAlpha = if (enabled) 1f else 0.38f
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -31,11 +35,12 @@ fun LabeledSlider(
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha),
|
||||
)
|
||||
Text(
|
||||
text = valueLabel,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = contentAlpha),
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
@@ -43,6 +48,7 @@ fun LabeledSlider(
|
||||
onValueChange = onValueChange,
|
||||
valueRange = valueRange,
|
||||
steps = steps,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -60,3 +66,18 @@ private fun LabeledSliderPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LabeledSliderDisabledPreview() {
|
||||
MaterialTheme {
|
||||
LabeledSlider(
|
||||
title = "Playback speed",
|
||||
valueLabel = "x0.7",
|
||||
value = 0.7f,
|
||||
valueRange = 0.5f..2f,
|
||||
steps = 10,
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -14,6 +14,7 @@ data class PlayerUiState(
|
||||
val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
|
||||
val playbackSpeed: Float = PlaybackPreferences.DEFAULT_SPEED,
|
||||
val breakBetweenPhrases: Duration = PlaybackPreferences.DEFAULT_BREAK_BETWEEN_PHRASES,
|
||||
val autoPause: Boolean = PlaybackPreferences.DEFAULT_AUTO_PAUSE,
|
||||
) {
|
||||
val canGoToPreviousSentence: Boolean
|
||||
get() = playerState.playbackTiming.position > 2.seconds
|
||||
@@ -21,8 +22,8 @@ data class PlayerUiState(
|
||||
val canGoToNextSentence: Boolean
|
||||
get() = (playerState.currentSubtitleIndex ?: Int.MAX_VALUE) < playerState.subtitleLines.lastIndex
|
||||
|
||||
/** True when either playback setting differs from its default, used to highlight the button. */
|
||||
/** True when any playback setting differs from its default, used to highlight the button. */
|
||||
val isPlaybackModified: Boolean
|
||||
get() = playbackSpeed != PlaybackPreferences.DEFAULT_SPEED ||
|
||||
breakBetweenPhrases != PlaybackPreferences.DEFAULT_BREAK_BETWEEN_PHRASES
|
||||
breakBetweenPhrases != PlaybackPreferences.DEFAULT_BREAK_BETWEEN_PHRASES || autoPause
|
||||
}
|
||||
|
||||
+7
@@ -46,6 +46,7 @@ class PlayerViewModel(
|
||||
subtitlePreferences = subtitlePreferences,
|
||||
playbackSpeed = playbackPreferences.speed,
|
||||
breakBetweenPhrases = playbackPreferences.breakBetweenPhrases,
|
||||
autoPause = playbackPreferences.autoPause,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
@@ -136,6 +137,12 @@ class PlayerViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoPause(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setAutoPause(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun setShowTranscription(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setShowTranscription(enabled)
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ fun PreferenceToggle(
|
||||
Column(modifier = Modifier.weight(1f).padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ private fun SubtitlePreferencesContent(
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.subtitle_preferences_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
PreferenceToggle(
|
||||
title = stringResource(Res.string.subtitle_transcription_label),
|
||||
|
||||
Reference in New Issue
Block a user