new: add adjustable break between phrases in playback settings
This commit is contained in:
+36
-28
@@ -2,37 +2,37 @@ 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.player.ui.components.LabeledSlider
|
||||
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
|
||||
import fr.ajaury.gwenedeg.resources.generated.resources.Res
|
||||
import fr.ajaury.gwenedeg.resources.generated.resources.break_between_phrases_title
|
||||
import fr.ajaury.gwenedeg.resources.generated.resources.break_between_phrases_value
|
||||
import fr.ajaury.gwenedeg.resources.generated.resources.playback_speed_title
|
||||
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Bottom sheet letting the user pick the audio playback speed between
|
||||
* [PlaybackPreferences.MIN_SPEED] and [PlaybackPreferences.MAX_SPEED].
|
||||
* Bottom sheet letting the user adjust playback: the audio [speed] and the [breakSeconds] silent
|
||||
* pause inserted between phrases.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PlaybackSpeedBottomSheet(
|
||||
speed: Float,
|
||||
breakSeconds: Float,
|
||||
onSpeedChange: (Float) -> Unit = {},
|
||||
onBreakChange: (Float) -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
) {
|
||||
@@ -42,7 +42,9 @@ fun PlaybackSpeedBottomSheet(
|
||||
) {
|
||||
PlaybackSpeedContent(
|
||||
speed = speed,
|
||||
breakSeconds = breakSeconds,
|
||||
onSpeedChange = onSpeedChange,
|
||||
onBreakChange = onBreakChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -50,40 +52,43 @@ fun PlaybackSpeedBottomSheet(
|
||||
@Composable
|
||||
private fun PlaybackSpeedContent(
|
||||
speed: Float,
|
||||
breakSeconds: Float,
|
||||
onSpeedChange: (Float) -> Unit = {},
|
||||
onBreakChange: (Float) -> Unit = {},
|
||||
) {
|
||||
// Number of discrete stops strictly between the endpoints (e.g. 0.5..1.0 by 0.1 → 4).
|
||||
val innerSteps = with(PlaybackPreferences) {
|
||||
val speedSteps = with(PlaybackPreferences) {
|
||||
(((MAX_SPEED - MIN_SPEED) / SPEED_STEP).roundToInt() - 1).coerceAtLeast(0)
|
||||
}
|
||||
val maxBreakSeconds = PlaybackPreferences.MAX_BREAK_BETWEEN_PHRASES.inWholeSeconds
|
||||
val breakStepSeconds = PlaybackPreferences.BREAK_STEP.inWholeSeconds
|
||||
val breakSteps = ((maxBreakSeconds / breakStepSeconds).toInt() - 1).coerceAtLeast(0)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.playback_speed_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Text(
|
||||
text = formatPlaybackSpeed(speed),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
LabeledSlider(
|
||||
title = stringResource(Res.string.playback_speed_title),
|
||||
valueLabel = formatPlaybackSpeed(speed),
|
||||
value = speed,
|
||||
onValueChange = onSpeedChange,
|
||||
valueRange = PlaybackPreferences.MIN_SPEED..PlaybackPreferences.MAX_SPEED,
|
||||
steps = innerSteps,
|
||||
steps = speedSteps,
|
||||
onValueChange = onSpeedChange,
|
||||
)
|
||||
LabeledSlider(
|
||||
title = stringResource(Res.string.break_between_phrases_title),
|
||||
valueLabel = stringResource(
|
||||
Res.string.break_between_phrases_value,
|
||||
breakSeconds.roundToInt().toString(),
|
||||
),
|
||||
value = breakSeconds,
|
||||
valueRange = 0f..maxBreakSeconds.toFloat(),
|
||||
steps = breakSteps,
|
||||
onValueChange = onBreakChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -98,6 +103,9 @@ internal fun formatPlaybackSpeed(speed: Float): String {
|
||||
@Composable
|
||||
private fun PlaybackSpeedContentPreview() {
|
||||
ChomBevTheme {
|
||||
PlaybackSpeedContent(speed = 0.7f)
|
||||
PlaybackSpeedContent(
|
||||
speed = 0.7f,
|
||||
breakSeconds = 2f,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ 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.ChomBevTheme
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -28,10 +27,10 @@ fun PlayerControl(
|
||||
modifier: Modifier = Modifier,
|
||||
playbackState: PlaybackState,
|
||||
playbackTiming: PlaybackTiming,
|
||||
breakBetweenPhrases: Duration,
|
||||
canGoToPreviousSentence: Boolean,
|
||||
canGoToNextSentence: Boolean,
|
||||
isSpeedActive: Boolean,
|
||||
playbackSpeed: Float,
|
||||
isPlaybackModified: Boolean,
|
||||
onSeek: (Duration) -> Unit = {},
|
||||
onMainPlayActionButtonClicked: () -> Unit = {},
|
||||
onPreviousSentenceClicked: () -> Unit = {},
|
||||
@@ -58,8 +57,7 @@ fun PlayerControl(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
SpeedPreferenceButton(
|
||||
isSpeedActive = isSpeedActive,
|
||||
playbackSpeed = playbackSpeed,
|
||||
isActive = isPlaybackModified,
|
||||
onSpeedClicked = onSpeedClicked,
|
||||
)
|
||||
}
|
||||
@@ -76,6 +74,7 @@ fun PlayerControl(
|
||||
|
||||
PlayButton(
|
||||
playbackState = playbackState,
|
||||
breakDuration = breakBetweenPhrases,
|
||||
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
|
||||
)
|
||||
|
||||
@@ -98,10 +97,10 @@ private fun PlayerControlPreview() {
|
||||
PlayerControl(
|
||||
playbackState = PlaybackState.PLAYING,
|
||||
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
|
||||
breakBetweenPhrases = 3.seconds,
|
||||
canGoToPreviousSentence = true,
|
||||
canGoToNextSentence = true,
|
||||
isSpeedActive = false,
|
||||
playbackSpeed = PlaybackPreferences.DEFAULT_SPEED,
|
||||
isPlaybackModified = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -113,10 +112,10 @@ private fun PlayerControlFirstSentencePreview() {
|
||||
PlayerControl(
|
||||
playbackState = PlaybackState.PLAYING,
|
||||
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
|
||||
breakBetweenPhrases = 3.seconds,
|
||||
canGoToPreviousSentence = false,
|
||||
canGoToNextSentence = true,
|
||||
playbackSpeed = PlaybackPreferences.DEFAULT_SPEED,
|
||||
isSpeedActive = false,
|
||||
isPlaybackModified = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -128,10 +127,10 @@ private fun PlayerControlActiveSpeedPreview() {
|
||||
PlayerControl(
|
||||
playbackState = PlaybackState.PLAYING,
|
||||
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
|
||||
breakBetweenPhrases = 3.seconds,
|
||||
canGoToPreviousSentence = true,
|
||||
canGoToNextSentence = true,
|
||||
playbackSpeed = 0.7f,
|
||||
isSpeedActive = true,
|
||||
isPlaybackModified = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -143,10 +142,10 @@ private fun PlayerControlLastSentencePreview() {
|
||||
PlayerControl(
|
||||
playbackState = PlaybackState.PLAYING,
|
||||
playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds),
|
||||
breakBetweenPhrases = 3.seconds,
|
||||
canGoToPreviousSentence = true,
|
||||
canGoToNextSentence = false,
|
||||
playbackSpeed = PlaybackPreferences.DEFAULT_SPEED,
|
||||
isSpeedActive = false,
|
||||
isPlaybackModified = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ fun PlayerScreen(
|
||||
onTextSizeDecrease = viewModel::decreaseSubtitleTextSize,
|
||||
onTextSizeIncrease = viewModel::increaseSubtitleTextSize,
|
||||
onSpeedChange = viewModel::setPlaybackSpeed,
|
||||
onBreakChange = viewModel::setBreakBetweenPhrases,
|
||||
onBackClicked = onBackClicked,
|
||||
)
|
||||
}
|
||||
@@ -93,6 +94,7 @@ fun PlayerScreen(
|
||||
onTextSizeDecrease: () -> Unit = {},
|
||||
onTextSizeIncrease: () -> Unit = {},
|
||||
onSpeedChange: (Float) -> Unit = {},
|
||||
onBreakChange: (Float) -> Unit = {},
|
||||
onBackClicked: () -> Unit = {},
|
||||
) {
|
||||
var showPreferences by remember { mutableStateOf(false) }
|
||||
@@ -171,10 +173,10 @@ fun PlayerScreen(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
playbackState = uiState.playerState.playbackState,
|
||||
playbackTiming = uiState.playerState.playbackTiming,
|
||||
breakBetweenPhrases = uiState.breakBetweenPhrases,
|
||||
canGoToPreviousSentence = uiState.canGoToPreviousSentence,
|
||||
canGoToNextSentence = uiState.canGoToNextSentence,
|
||||
playbackSpeed = uiState.playbackSpeed,
|
||||
isSpeedActive = uiState.isSpeedActive,
|
||||
isPlaybackModified = uiState.isPlaybackModified,
|
||||
onSeek = onSeekToTimePart,
|
||||
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
|
||||
onPreviousSentenceClicked = onPreviousSentenceClicked,
|
||||
@@ -197,7 +199,9 @@ fun PlayerScreen(
|
||||
if (showSpeed) {
|
||||
PlaybackSpeedBottomSheet(
|
||||
speed = uiState.playbackSpeed,
|
||||
breakSeconds = uiState.breakBetweenPhrases.inWholeSeconds.toFloat(),
|
||||
onSpeedChange = onSpeedChange,
|
||||
onBreakChange = onBreakChange,
|
||||
onDismiss = { showSpeed = false },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package fr.ajaury.gwenedeg.player.ui.components
|
||||
|
||||
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.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
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.theme.ChomBevTheme
|
||||
|
||||
@Composable
|
||||
fun LabeledSlider(
|
||||
title: String,
|
||||
valueLabel: String,
|
||||
value: Float,
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
steps: Int,
|
||||
onValueChange: (Float) -> Unit = {},
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Text(
|
||||
text = valueLabel,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
valueRange = valueRange,
|
||||
steps = steps,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LabeledSliderPreview() {
|
||||
ChomBevTheme {
|
||||
LabeledSlider(
|
||||
title = "Playback speed",
|
||||
valueLabel = "x0.7",
|
||||
value = 0.7f,
|
||||
valueRange = 0.5f..2f,
|
||||
steps = 10,
|
||||
)
|
||||
}
|
||||
}
|
||||
+68
-10
@@ -1,5 +1,9 @@
|
||||
package fr.ajaury.gwenedeg.player.ui.components
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -7,8 +11,12 @@ 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.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -20,27 +28,66 @@ import fr.ajaury.gwenedeg.resources.generated.resources.player_play
|
||||
import fr.ajaury.gwenedeg.resources.generated.resources.player_replay
|
||||
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private val ButtonSize = 60.dp
|
||||
private val BreakRingSize = 68.dp
|
||||
|
||||
@Composable
|
||||
fun PlayButton(
|
||||
playbackState: PlaybackState,
|
||||
breakDuration: Duration = Duration.ZERO,
|
||||
onMainPlayActionButtonClicked: () -> Unit = {},
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.size(60.dp),
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
onClick = onMainPlayActionButtonClicked,
|
||||
val isBreaking = playbackState == PlaybackState.BREAKING && breakDuration > Duration.ZERO
|
||||
val breakProgress = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(isBreaking, breakDuration) {
|
||||
if (isBreaking) {
|
||||
breakProgress.snapTo(0f)
|
||||
breakProgress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = breakDuration.inWholeMilliseconds.toInt(),
|
||||
easing = LinearEasing,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
breakProgress.snapTo(0f)
|
||||
}
|
||||
}
|
||||
|
||||
// The ring reserves its footprint at all times so the transport row does not jump when a break
|
||||
// starts; the indicator itself is only drawn while breaking.
|
||||
Box(
|
||||
modifier = Modifier.size(BreakRingSize),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = playbackState.toActionIcon(),
|
||||
contentDescription = playbackState.toActionContentDescription(),
|
||||
)
|
||||
if (isBreaking) {
|
||||
CircularProgressIndicator(
|
||||
progress = { breakProgress.value },
|
||||
modifier = Modifier.size(BreakRingSize),
|
||||
strokeWidth = 3.dp,
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.size(ButtonSize),
|
||||
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.PLAYING, PlaybackState.BREAKING -> Icons.Default.Pause
|
||||
PlaybackState.ENDED -> Icons.Default.Replay
|
||||
PlaybackState.PAUSED, PlaybackState.IDLE -> Icons.Default.PlayArrow
|
||||
}
|
||||
@@ -48,7 +95,7 @@ private fun PlaybackState.toActionIcon(): ImageVector =
|
||||
@Composable
|
||||
private fun PlaybackState.toActionContentDescription(): String =
|
||||
when (this) {
|
||||
PlaybackState.PLAYING -> stringResource(Res.string.player_pause)
|
||||
PlaybackState.PLAYING, PlaybackState.BREAKING -> stringResource(Res.string.player_pause)
|
||||
PlaybackState.ENDED -> stringResource(Res.string.player_replay)
|
||||
PlaybackState.PAUSED, PlaybackState.IDLE -> stringResource(Res.string.player_play)
|
||||
}
|
||||
@@ -63,6 +110,17 @@ private fun PlayButtonPlayingPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PlayButtonBreakingPreview() {
|
||||
ChomBevTheme {
|
||||
PlayButton(
|
||||
playbackState = PlaybackState.BREAKING,
|
||||
breakDuration = 3.seconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PlayButtonPausedPreview() {
|
||||
|
||||
+20
-24
@@ -1,40 +1,38 @@
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Speed
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.resources.generated.resources.Res
|
||||
import fr.ajaury.gwenedeg.resources.generated.resources.playback_settings_action
|
||||
import fr.ajaury.gwenedeg.theme.ChomBevTheme
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* Opens the playback settings sheet. Tinted with the primary color when a playback setting (speed or
|
||||
* break between phrases) differs from its default.
|
||||
*/
|
||||
@Composable
|
||||
fun SpeedPreferenceButton(
|
||||
isSpeedActive: Boolean,
|
||||
playbackSpeed: Float,
|
||||
isActive: Boolean,
|
||||
onSpeedClicked: () -> Unit = {},
|
||||
) {
|
||||
TextButton(
|
||||
IconButton(
|
||||
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,
|
||||
Icon(
|
||||
imageVector = Icons.Default.Speed,
|
||||
contentDescription = stringResource(Res.string.playback_settings_action),
|
||||
tint = if (isActive) MaterialTheme.colorScheme.primary else LocalContentColor.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -44,19 +42,17 @@ fun SpeedPreferenceButton(
|
||||
private fun SpeedPreferenceButtonDefaultPreview() {
|
||||
ChomBevTheme {
|
||||
SpeedPreferenceButton(
|
||||
isSpeedActive = false,
|
||||
playbackSpeed = 1f,
|
||||
isActive = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SpeedPreferenceButtonPreview() {
|
||||
private fun SpeedPreferenceButtonActivePreview() {
|
||||
ChomBevTheme {
|
||||
SpeedPreferenceButton(
|
||||
isSpeedActive = true,
|
||||
playbackSpeed = 0.7f,
|
||||
isActive = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -4,6 +4,7 @@ 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
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class PlayerUiState(
|
||||
@@ -12,6 +13,7 @@ data class PlayerUiState(
|
||||
val playerState: PlayerState = PlayerState(),
|
||||
val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
|
||||
val playbackSpeed: Float = PlaybackPreferences.DEFAULT_SPEED,
|
||||
val breakBetweenPhrases: Duration = PlaybackPreferences.DEFAULT_BREAK_BETWEEN_PHRASES,
|
||||
) {
|
||||
val canGoToPreviousSentence: Boolean
|
||||
get() = playerState.playbackTiming.position > 2.seconds
|
||||
@@ -19,6 +21,8 @@ data class PlayerUiState(
|
||||
val canGoToNextSentence: Boolean
|
||||
get() = (playerState.currentSubtitleIndex ?: Int.MAX_VALUE) < playerState.subtitleLines.lastIndex
|
||||
|
||||
val isSpeedActive: Boolean
|
||||
get() = playbackSpeed != PlaybackPreferences.DEFAULT_SPEED
|
||||
/** True when either 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
|
||||
}
|
||||
|
||||
+32
-9
@@ -46,6 +46,7 @@ class PlayerViewModel(
|
||||
playerState = playerState,
|
||||
subtitlePreferences = subtitlePreferences,
|
||||
playbackSpeed = playbackPreferences.speed,
|
||||
breakBetweenPhrases = playbackPreferences.breakBetweenPhrases,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
@@ -79,27 +80,37 @@ class PlayerViewModel(
|
||||
}
|
||||
|
||||
fun performMainPlayAction() {
|
||||
when (uiState.value.playerState.playbackState) {
|
||||
PlaybackState.PLAYING -> playbackRepository.pause()
|
||||
PlaybackState.ENDED -> playbackRepository.replay()
|
||||
PlaybackState.PAUSED, PlaybackState.IDLE -> playbackRepository.play()
|
||||
viewModelScope.launch {
|
||||
when (uiState.value.playerState.playbackState) {
|
||||
PlaybackState.PLAYING, PlaybackState.BREAKING -> playbackRepository.pause()
|
||||
PlaybackState.ENDED -> playbackRepository.replay()
|
||||
PlaybackState.PAUSED, PlaybackState.IDLE -> playbackRepository.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun seekToTimePart(progress: Duration) {
|
||||
playbackRepository.seekTo(progress)
|
||||
viewModelScope.launch {
|
||||
playbackRepository.seekTo(progress)
|
||||
}
|
||||
}
|
||||
|
||||
fun seekToSentence(sentenceIndex: Int) {
|
||||
playbackRepository.seekToSentence(sentenceIndex)
|
||||
viewModelScope.launch {
|
||||
playbackRepository.seekToSentence(sentenceIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun goToPreviousSentence() {
|
||||
playbackRepository.goToPreviousSentence()
|
||||
viewModelScope.launch {
|
||||
playbackRepository.goToPreviousSentence()
|
||||
}
|
||||
}
|
||||
|
||||
fun goToNextSentence() {
|
||||
playbackRepository.goToNextSentence()
|
||||
viewModelScope.launch {
|
||||
playbackRepository.goToNextSentence()
|
||||
}
|
||||
}
|
||||
|
||||
fun setPlaybackSpeed(speed: Float) {
|
||||
@@ -114,6 +125,16 @@ class PlayerViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setBreakBetweenPhrases(seconds: Float) {
|
||||
val clampedSeconds = seconds
|
||||
.roundToInt()
|
||||
.coerceIn(0, PlaybackPreferences.MAX_BREAK_BETWEEN_PHRASES.inWholeSeconds.toInt())
|
||||
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setBreakBetweenPhrases(clampedSeconds.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
fun setShowTranscription(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setShowTranscription(enabled)
|
||||
@@ -143,7 +164,9 @@ class PlayerViewModel(
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
playbackRepository.stop()
|
||||
viewModelScope.launch {
|
||||
playbackRepository.stop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
||||
Reference in New Issue
Block a user