new: add previous and next sentence navigation in PlayerControl and PlayerViewModel
This commit is contained in:
@@ -3,14 +3,18 @@ package fr.ajaury.gwenedeg.player.ui
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Pause
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Replay
|
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.Button
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -20,9 +24,11 @@ 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.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import fr.ajaury.gwenedeg.player.model.PlaybackState
|
import fr.ajaury.gwenedeg.player.model.PlaybackState
|
||||||
import fr.ajaury.gwenedeg.player.model.PlaybackTiming
|
import fr.ajaury.gwenedeg.player.model.PlaybackTiming
|
||||||
|
import fr.ajaury.gwenedeg.theme.GwenedegTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlayerControl(
|
fun PlayerControl(
|
||||||
@@ -30,6 +36,8 @@ fun PlayerControl(
|
|||||||
playbackTiming: PlaybackTiming,
|
playbackTiming: PlaybackTiming,
|
||||||
onSeek: (Float) -> Unit = {},
|
onSeek: (Float) -> Unit = {},
|
||||||
onMainPlayActionButtonClicked: () -> Unit = {},
|
onMainPlayActionButtonClicked: () -> Unit = {},
|
||||||
|
onPreviousSentenceClicked: () -> Unit = {},
|
||||||
|
onNextSentenceClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -51,15 +59,40 @@ fun PlayerControl(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
Button(
|
Row(
|
||||||
modifier = Modifier.size(60.dp),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
contentPadding = PaddingValues(0.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
onClick = onMainPlayActionButtonClicked,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
IconButton(
|
||||||
imageVector = playbackState.toActionIcon(),
|
modifier = Modifier.size(60.dp),
|
||||||
contentDescription = playbackState.toActionContentDescription(),
|
onClick = onPreviousSentenceClicked,
|
||||||
)
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.SkipPrevious,
|
||||||
|
contentDescription = "Previous sentence",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.size(60.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
onClick = onMainPlayActionButtonClicked,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = playbackState.toActionIcon(),
|
||||||
|
contentDescription = playbackState.toActionContentDescription(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(60.dp),
|
||||||
|
onClick = onNextSentenceClicked,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.SkipNext,
|
||||||
|
contentDescription = "Next sentence",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,3 +110,14 @@ private fun PlaybackState.toActionContentDescription(): String =
|
|||||||
PlaybackState.ENDED -> "Replay"
|
PlaybackState.ENDED -> "Replay"
|
||||||
PlaybackState.PAUSED, PlaybackState.IDLE -> "Play"
|
PlaybackState.PAUSED, PlaybackState.IDLE -> "Play"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PlayerControlPreview() {
|
||||||
|
GwenedegTheme {
|
||||||
|
PlayerControl(
|
||||||
|
playbackState = PlaybackState.PLAYING,
|
||||||
|
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ fun PlayerScreen(
|
|||||||
onMainPlayActionButtonClicked = viewModel::performMainPlayAction,
|
onMainPlayActionButtonClicked = viewModel::performMainPlayAction,
|
||||||
onSeekToTimePart = viewModel::seekToTimePart,
|
onSeekToTimePart = viewModel::seekToTimePart,
|
||||||
onSeekToSentence = viewModel::seekToSentence,
|
onSeekToSentence = viewModel::seekToSentence,
|
||||||
|
onPreviousSentenceClicked = viewModel::goToPreviousSentence,
|
||||||
|
onNextSentenceClicked = viewModel::goToNextSentence,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,8 @@ fun PlayerScreen(
|
|||||||
onMainPlayActionButtonClicked: () -> Unit = {},
|
onMainPlayActionButtonClicked: () -> Unit = {},
|
||||||
onSeekToTimePart: (progress: Float) -> Unit = {},
|
onSeekToTimePart: (progress: Float) -> Unit = {},
|
||||||
onSeekToSentence: (position: Int) -> Unit = {},
|
onSeekToSentence: (position: Int) -> Unit = {},
|
||||||
|
onPreviousSentenceClicked: () -> Unit = {},
|
||||||
|
onNextSentenceClicked: () -> Unit = {},
|
||||||
onBackClicked: () -> Unit = {},
|
onBackClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -106,6 +110,8 @@ fun PlayerScreen(
|
|||||||
playbackTiming = uiState.playbackTiming,
|
playbackTiming = uiState.playbackTiming,
|
||||||
onSeek = onSeekToTimePart,
|
onSeek = onSeekToTimePart,
|
||||||
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
|
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
|
||||||
|
onPreviousSentenceClicked = onPreviousSentenceClicked,
|
||||||
|
onNextSentenceClicked = onNextSentenceClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -165,6 +165,33 @@ class PlayerViewModel(
|
|||||||
audioPlayer.seekTo(matchingSubtitleLine.startMs.milliseconds)
|
audioPlayer.seekTo(matchingSubtitleLine.startMs.milliseconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun goToPreviousSentence() {
|
||||||
|
val lines = subtitle.value.lines
|
||||||
|
val currentIndex = currentSentenceIndex() ?: return
|
||||||
|
val currentLine = lines.getOrNull(currentIndex) ?: return
|
||||||
|
val elapsedInSentence =
|
||||||
|
audioPlayer.currentPosition.inWholeMilliseconds - currentLine.startMs
|
||||||
|
val targetIndex = if (elapsedInSentence < PREVIOUS_SENTENCE_THRESHOLD.inWholeMilliseconds) {
|
||||||
|
(currentIndex - 1)
|
||||||
|
} else {
|
||||||
|
currentIndex
|
||||||
|
}.coerceAtLeast(0)
|
||||||
|
|
||||||
|
seekToSentence(targetIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goToNextSentence() {
|
||||||
|
val lines = subtitle.value.lines
|
||||||
|
val currentIndex = currentSentenceIndex() ?: return
|
||||||
|
seekToSentence((currentIndex + 1).coerceAtMost(lines.lastIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentSentenceIndex(): Int? =
|
||||||
|
getCurrentSubtitleIndex(
|
||||||
|
subtitle = subtitle.value,
|
||||||
|
progressMs = audioPlayer.currentPosition.inWholeMilliseconds,
|
||||||
|
)
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
audioPlayer.stop()
|
audioPlayer.stop()
|
||||||
}
|
}
|
||||||
@@ -177,5 +204,6 @@ class PlayerViewModel(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val POSITION_POLL_INTERVAL = 50.milliseconds
|
private val POSITION_POLL_INTERVAL = 50.milliseconds
|
||||||
|
private val PREVIOUS_SENTENCE_THRESHOLD = 4.seconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user