new: add playback speed control

This commit is contained in:
2026-07-03 10:37:28 +02:00
parent 9aaffdc363
commit 3847f46adb
20 changed files with 618 additions and 80 deletions
@@ -0,0 +1,100 @@
package fr.ajaury.gwenedeg.player.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
import fr.ajaury.gwenedeg.theme.GwenedegTheme
import kotlin.math.roundToInt
/**
* Bottom sheet letting the user pick the audio playback speed between
* [PlaybackPreferences.MIN_SPEED] and [PlaybackPreferences.MAX_SPEED].
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlaybackSpeedBottomSheet(
speed: Float,
onSpeedChange: (Float) -> Unit = {},
onDismiss: () -> Unit = {},
sheetState: SheetState = rememberModalBottomSheetState(),
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
PlaybackSpeedContent(
speed = speed,
onSpeedChange = onSpeedChange,
)
}
}
@Composable
private fun PlaybackSpeedContent(
speed: Float,
onSpeedChange: (Float) -> Unit = {},
) {
// Number of discrete stops strictly between the endpoints (e.g. 0.5..1.0 by 0.1 → 4).
val innerSteps = with(PlaybackPreferences) {
(((MAX_SPEED - MIN_SPEED) / SPEED_STEP).roundToInt() - 1).coerceAtLeast(0)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Vitesse de lecture",
style = MaterialTheme.typography.titleMedium,
)
Text(
text = formatPlaybackSpeed(speed),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
}
Slider(
value = speed,
onValueChange = onSpeedChange,
valueRange = PlaybackPreferences.MIN_SPEED..PlaybackPreferences.MAX_SPEED,
steps = innerSteps,
)
}
}
/** Formats a speed multiplier as a short label, e.g. 1.0 → "x1", 0.7 → "x0.7". */
internal fun formatPlaybackSpeed(speed: Float): String {
val tenths = (speed * 10).roundToInt()
return if (tenths % 10 == 0) "x${tenths / 10}" else "x${tenths / 10}.${tenths % 10}"
}
@Preview
@Composable
private fun PlaybackSpeedContentPreview() {
GwenedegTheme {
PlaybackSpeedContent(speed = 0.7f)
}
}
@@ -1,33 +1,24 @@
package fr.ajaury.gwenedeg.player.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.ajaury.gwenedeg.player.model.PlaybackState
import fr.ajaury.gwenedeg.player.model.PlaybackTiming
import fr.ajaury.gwenedeg.player.ui.components.NextButton
import fr.ajaury.gwenedeg.player.ui.components.PlayButton
import fr.ajaury.gwenedeg.player.ui.components.PlaybackProgress
import fr.ajaury.gwenedeg.player.ui.components.PreviousButton
import fr.ajaury.gwenedeg.player.ui.components.SpeedPreferenceButton
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
import fr.ajaury.gwenedeg.theme.GwenedegTheme
@Composable
@@ -35,87 +26,69 @@ fun PlayerControl(
modifier: Modifier = Modifier,
playbackState: PlaybackState,
playbackTiming: PlaybackTiming,
canGoToPreviousSentence: Boolean,
canGoToNextSentence: Boolean,
isSpeedActive: Boolean,
playbackSpeed: Float,
onSeek: (Float) -> Unit = {},
onMainPlayActionButtonClicked: () -> Unit = {},
onPreviousSentenceClicked: () -> Unit = {},
onNextSentenceClicked: () -> Unit = {},
canGoToPreviousSentence: Boolean = true,
canGoToNextSentence: Boolean = true,
onSpeedClicked: () -> Unit = {},
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
var seekProgress by remember { mutableStateOf<Float?>(null) }
Slider(
value = seekProgress ?: playbackTiming.progress,
onValueChange = { seekProgress = it },
onValueChangeFinished = {
seekProgress?.let { fraction ->
if (playbackTiming.durationMs > 0) {
onSeek(fraction)
}
}
seekProgress = null
},
modifier = Modifier.fillMaxWidth(),
PlaybackProgress(
playbackTiming = playbackTiming,
onSeek = onSeek,
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
IconButton(
modifier = Modifier.size(60.dp),
enabled = canGoToPreviousSentence,
onClick = onPreviousSentenceClicked,
// Left region: the speed button is centered in the space left of the transport group.
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.SkipPrevious,
contentDescription = "Previous sentence",
SpeedPreferenceButton(
isSpeedActive = isSpeedActive,
playbackSpeed = playbackSpeed,
onSpeedClicked = onSpeedClicked,
)
}
Button(
modifier = Modifier.size(60.dp),
contentPadding = PaddingValues(0.dp),
onClick = onMainPlayActionButtonClicked,
// Transport group: centered in the full width by the equal-weight cells on either side.
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = playbackState.toActionIcon(),
contentDescription = playbackState.toActionContentDescription(),
PreviousButton(
canGoToPreviousSentence = canGoToPreviousSentence,
onPreviousSentenceClicked = onPreviousSentenceClicked,
)
PlayButton(
playbackState = playbackState,
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
)
NextButton(
canGoToNextSentence = canGoToNextSentence,
onNextSentenceClicked = onNextSentenceClicked,
)
}
IconButton(
modifier = Modifier.size(60.dp),
enabled = canGoToNextSentence,
onClick = onNextSentenceClicked,
) {
Icon(
imageVector = Icons.Default.SkipNext,
contentDescription = "Next sentence",
)
}
// Right region: mirrors the left weight so the transport group stays centered.
Spacer(modifier = Modifier.weight(1f))
}
}
}
private fun PlaybackState.toActionIcon(): ImageVector =
when (this) {
PlaybackState.PLAYING -> Icons.Default.Pause
PlaybackState.ENDED -> Icons.Default.Replay
PlaybackState.PAUSED, PlaybackState.IDLE -> Icons.Default.PlayArrow
}
private fun PlaybackState.toActionContentDescription(): String =
when (this) {
PlaybackState.PLAYING -> "Pause"
PlaybackState.ENDED -> "Replay"
PlaybackState.PAUSED, PlaybackState.IDLE -> "Play"
}
@Preview
@Composable
private fun PlayerControlPreview() {
@@ -123,6 +96,10 @@ private fun PlayerControlPreview() {
PlayerControl(
playbackState = PlaybackState.PLAYING,
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
canGoToPreviousSentence = true,
canGoToNextSentence = true,
isSpeedActive = false,
playbackSpeed = PlaybackPreferences.DEFAULT_SPEED,
)
}
}
@@ -135,6 +112,24 @@ private fun PlayerControlFirstSentencePreview() {
playbackState = PlaybackState.PLAYING,
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
canGoToPreviousSentence = false,
canGoToNextSentence = true,
playbackSpeed = PlaybackPreferences.DEFAULT_SPEED,
isSpeedActive = false,
)
}
}
@Preview
@Composable
private fun PlayerControlActiveSpeedPreview() {
GwenedegTheme {
PlayerControl(
playbackState = PlaybackState.PLAYING,
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
canGoToPreviousSentence = true,
canGoToNextSentence = true,
playbackSpeed = 0.7f,
isSpeedActive = true,
)
}
}
@@ -146,7 +141,10 @@ private fun PlayerControlLastSentencePreview() {
PlayerControl(
playbackState = PlaybackState.PLAYING,
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
canGoToPreviousSentence = true,
canGoToNextSentence = false,
playbackSpeed = PlaybackPreferences.DEFAULT_SPEED,
isSpeedActive = false,
)
}
}
@@ -32,6 +32,7 @@ import fr.ajaury.gwenedeg.player.model.PlaybackTiming
import fr.ajaury.gwenedeg.player.model.PlayerState
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerUiState
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
import fr.ajaury.gwenedeg.preferences.ui.SubtitlePreferencesBottomSheet
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
import fr.ajaury.gwenedeg.theme.GwenedegTheme
@@ -64,6 +65,7 @@ fun PlayerScreen(
onNextSentenceClicked = viewModel::goToNextSentence,
onShowTranscriptionChange = viewModel::setShowTranscription,
onShowTranslationChange = viewModel::setShowTranslation,
onSpeedChange = viewModel::setPlaybackSpeed,
onBackClicked = onBackClicked,
)
}
@@ -79,9 +81,11 @@ fun PlayerScreen(
onNextSentenceClicked: () -> Unit = {},
onShowTranscriptionChange: (Boolean) -> Unit = {},
onShowTranslationChange: (Boolean) -> Unit = {},
onSpeedChange: (Float) -> Unit = {},
onBackClicked: () -> Unit = {},
) {
var showPreferences by remember { mutableStateOf(false) }
var showSpeed by remember { mutableStateOf(false) }
Scaffold(
topBar = {
@@ -146,12 +150,15 @@ fun PlayerScreen(
modifier = Modifier.padding(horizontal = 8.dp),
playbackState = uiState.playerState.playbackState,
playbackTiming = uiState.playerState.playbackTiming,
canGoToPreviousSentence = uiState.canGoToPreviousSentence,
canGoToNextSentence = uiState.canGoToNextSentence,
playbackSpeed = uiState.playbackSpeed,
isSpeedActive = uiState.isSpeedActive,
onSeek = onSeekToTimePart,
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
onPreviousSentenceClicked = onPreviousSentenceClicked,
onNextSentenceClicked = onNextSentenceClicked,
canGoToPreviousSentence = uiState.canGoToPreviousSentence,
canGoToNextSentence = uiState.canGoToNextSentence,
onSpeedClicked = { showSpeed = true },
)
}
@@ -163,6 +170,14 @@ fun PlayerScreen(
onDismiss = { showPreferences = false },
)
}
if (showSpeed) {
PlaybackSpeedBottomSheet(
speed = uiState.playbackSpeed,
onSpeedChange = onSpeedChange,
onDismiss = { showSpeed = false },
)
}
}
}
@@ -0,0 +1,49 @@
package fr.ajaury.gwenedeg.player.ui.components
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.ajaury.gwenedeg.theme.GwenedegTheme
@Composable
fun NextButton(
canGoToNextSentence: Boolean,
onNextSentenceClicked: () -> Unit = {},
) {
IconButton(
modifier = Modifier.size(60.dp),
enabled = canGoToNextSentence,
onClick = onNextSentenceClicked,
) {
Icon(
imageVector = Icons.Default.SkipNext,
contentDescription = "Next sentence",
)
}
}
@Preview
@Composable
private fun NextButtonCanGoToNextSentencePreview() {
GwenedegTheme {
NextButton(
canGoToNextSentence = true,
)
}
}
@Preview
@Composable
private fun NextButtonCanNotGoToNextSentencePreview() {
GwenedegTheme {
NextButton(
canGoToNextSentence = false,
)
}
}
@@ -0,0 +1,88 @@
package fr.ajaury.gwenedeg.player.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.ajaury.gwenedeg.player.model.PlaybackState
import fr.ajaury.gwenedeg.theme.GwenedegTheme
@Composable
fun PlayButton(
playbackState: PlaybackState,
onMainPlayActionButtonClicked: () -> Unit = {},
) {
Button(
modifier = Modifier.size(60.dp),
contentPadding = PaddingValues(0.dp),
onClick = onMainPlayActionButtonClicked,
) {
Icon(
imageVector = playbackState.toActionIcon(),
contentDescription = playbackState.toActionContentDescription(),
)
}
}
private fun PlaybackState.toActionIcon(): ImageVector =
when (this) {
PlaybackState.PLAYING -> Icons.Default.Pause
PlaybackState.ENDED -> Icons.Default.Replay
PlaybackState.PAUSED, PlaybackState.IDLE -> Icons.Default.PlayArrow
}
private fun PlaybackState.toActionContentDescription(): String =
when (this) {
PlaybackState.PLAYING -> "Pause"
PlaybackState.ENDED -> "Replay"
PlaybackState.PAUSED, PlaybackState.IDLE -> "Play"
}
@Preview
@Composable
private fun PlayButtonPlayingPreview() {
GwenedegTheme {
PlayButton(
playbackState = PlaybackState.PLAYING,
)
}
}
@Preview
@Composable
private fun PlayButtonPausedPreview() {
GwenedegTheme {
PlayButton(
playbackState = PlaybackState.PAUSED,
)
}
}
@Preview
@Composable
private fun PlayButtonEndedPreview() {
GwenedegTheme {
PlayButton(
playbackState = PlaybackState.ENDED,
)
}
}
@Preview
@Composable
private fun PlayButtonIdlePreview() {
GwenedegTheme {
PlayButton(
playbackState = PlaybackState.IDLE,
)
}
}
@@ -0,0 +1,66 @@
package fr.ajaury.gwenedeg.player.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import fr.ajaury.gwenedeg.player.model.PlaybackTiming
import fr.ajaury.gwenedeg.theme.GwenedegTheme
@Composable
fun PlaybackProgress(
playbackTiming: PlaybackTiming,
onSeek: (Float) -> Unit = {},
) {
var seekProgress by remember { mutableStateOf<Float?>(null) }
Slider(
value = seekProgress ?: playbackTiming.progress,
onValueChange = { seekProgress = it },
onValueChangeFinished = {
seekProgress?.let { fraction ->
if (playbackTiming.durationMs > 0) {
onSeek(fraction)
}
}
seekProgress = null
},
modifier = Modifier.fillMaxWidth(),
)
}
@Preview
@Composable
private fun PlaybackProgressPreview() {
GwenedegTheme {
PlaybackProgress(
playbackTiming = PlaybackTiming(0, 0),
)
}
}
@Preview
@Composable
private fun PlaybackProgressInProgressPreview() {
GwenedegTheme {
PlaybackProgress(
playbackTiming = PlaybackTiming(7000, 10000),
)
}
}
@Preview
@Composable
private fun PlaybackProgressEndedPreview() {
GwenedegTheme {
PlaybackProgress(
playbackTiming = PlaybackTiming(10000, 10000),
)
}
}
@@ -0,0 +1,49 @@
package fr.ajaury.gwenedeg.player.ui.components
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.ajaury.gwenedeg.theme.GwenedegTheme
@Composable
fun PreviousButton(
canGoToPreviousSentence: Boolean,
onPreviousSentenceClicked: () -> Unit = {},
) {
IconButton(
modifier = Modifier.size(60.dp),
enabled = canGoToPreviousSentence,
onClick = onPreviousSentenceClicked,
) {
Icon(
imageVector = Icons.Default.SkipPrevious,
contentDescription = "Previous sentence",
)
}
}
@Preview
@Composable
private fun PreviousButtonCanGoToPreviousSentencePreview() {
GwenedegTheme {
PreviousButton(
canGoToPreviousSentence = true,
)
}
}
@Preview
@Composable
private fun PreviousButtonCanNotGoToPreviousSentencePreview() {
GwenedegTheme {
PreviousButton(
canGoToPreviousSentence = false,
)
}
}
@@ -0,0 +1,62 @@
package fr.ajaury.gwenedeg.player.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import fr.ajaury.gwenedeg.player.ui.formatPlaybackSpeed
import fr.ajaury.gwenedeg.theme.GwenedegTheme
@Composable
fun SpeedPreferenceButton(
isSpeedActive: Boolean,
playbackSpeed: Float,
onSpeedClicked: () -> Unit = {},
) {
TextButton(
modifier = Modifier.size(60.dp),
contentPadding = PaddingValues(0.dp),
onClick = onSpeedClicked,
colors = ButtonDefaults.textButtonColors(
contentColor = if (isSpeedActive) {
MaterialTheme.colorScheme.primary
} else {
LocalContentColor.current
},
),
) {
Text(
text = formatPlaybackSpeed(playbackSpeed),
style = MaterialTheme.typography.labelLarge,
)
}
}
@Preview
@Composable
private fun SpeedPreferenceButtonDefaultPreview() {
GwenedegTheme {
SpeedPreferenceButton(
isSpeedActive = false,
playbackSpeed = 1f,
)
}
}
@Preview
@Composable
private fun SpeedPreferenceButtonPreview() {
GwenedegTheme {
SpeedPreferenceButton(
isSpeedActive = true,
playbackSpeed = 0.7f,
)
}
}
@@ -2,16 +2,21 @@ package fr.ajaury.gwenedeg.player.ui.viewmodel
import fr.ajaury.gwenedeg.core.model.Phrase
import fr.ajaury.gwenedeg.player.model.PlayerState
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
data class PlayerUiState(
val recordTitle: Phrase = Phrase(transcription = ""),
val playerState: PlayerState = PlayerState(),
val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
val playbackSpeed: Float = PlaybackPreferences.DEFAULT_SPEED,
) {
val canGoToPreviousSentence: Boolean
get() = playerState.playbackTiming.positionMs > 2000
val canGoToNextSentence: Boolean
get() = (playerState.currentSubtitleIndex ?: Int.MAX_VALUE) < playerState.subtitleLines.lastIndex
val isSpeedActive: Boolean
get() = playbackSpeed != PlaybackPreferences.DEFAULT_SPEED
}
@@ -8,6 +8,7 @@ import fr.ajaury.gwenedeg.player.domain.AudioSessionManager
import fr.ajaury.gwenedeg.player.domain.PlaybackRepository
import fr.ajaury.gwenedeg.player.model.PlaybackState
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
import fr.ajaury.gwenedeg.records.domain.RecordRepository
import fr.ajaury.gwenedeg.records.model.Record
import kotlinx.coroutines.flow.MutableStateFlow
@@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@@ -34,11 +36,13 @@ class PlayerViewModel(
record,
playbackRepository.playerState,
preferencesRepository.subtitlePreferences,
) { record, playerState, subtitlePreferences ->
preferencesRepository.playbackPreferences,
) { record, playerState, subtitlePreferences, playbackPreferences ->
PlayerUiState(
recordTitle = record?.title ?: Phrase(transcription = ""),
playerState = playerState,
subtitlePreferences = subtitlePreferences,
playbackSpeed = playbackPreferences.speed,
)
}.stateIn(
scope = viewModelScope,
@@ -57,6 +61,7 @@ class PlayerViewModel(
if (alreadyLoadedRecord != null) {
logger.debug("Record $recordId already loaded")
playbackRepository.load(record = alreadyLoadedRecord)
applyCurrentSpeed()
return@launch
}
@@ -67,10 +72,15 @@ class PlayerViewModel(
}
playbackRepository.load(record = loadedRecord)
applyCurrentSpeed()
record.value = loadedRecord
}
}
private fun applyCurrentSpeed() {
playbackRepository.setSpeed(preferencesRepository.playbackPreferences.value.speed)
}
fun performMainPlayAction() {
when (uiState.value.playerState.playbackState) {
PlaybackState.PLAYING -> playbackRepository.pause()
@@ -97,6 +107,16 @@ class PlayerViewModel(
playbackRepository.goToNextSentence()
}
fun setPlaybackSpeed(speed: Float) {
val steppedSpeed = (speed * 10).roundToInt() / 10f
val clampedSpeed = steppedSpeed.coerceIn(
PlaybackPreferences.MIN_SPEED,
PlaybackPreferences.MAX_SPEED,
)
preferencesRepository.setPlaybackSpeed(clampedSpeed)
playbackRepository.setSpeed(clampedSpeed)
}
fun setShowTranscription(enabled: Boolean) {
preferencesRepository.setShowTranscription(enabled)
}