refactor: simplify player state
This commit is contained in:
+32
-14
@@ -1,11 +1,13 @@
|
||||
package fr.ajaury.gwenedeg.player.data
|
||||
|
||||
import fr.ajaury.gwenedeg.core.logging.domain.Logger
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.core.model.Record
|
||||
import fr.ajaury.gwenedeg.player.domain.AudioPlayer
|
||||
import fr.ajaury.gwenedeg.player.domain.PlaybackRepository
|
||||
import fr.ajaury.gwenedeg.player.model.PlaybackState
|
||||
import fr.ajaury.gwenedeg.player.model.PlaybackTiming
|
||||
import fr.ajaury.gwenedeg.player.model.PlayerState
|
||||
import fr.ajaury.gwenedeg.resourcereader.data.domain.ResourceReader
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase
|
||||
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository
|
||||
@@ -16,7 +18,6 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
@@ -33,15 +34,15 @@ internal class PlaybackRepositoryImpl(
|
||||
private val getCurrentSubtitleIndex: GetCurrentSubtitleIndexUseCase,
|
||||
private val logger: Logger,
|
||||
) : PlaybackRepository {
|
||||
private val subtitleState = MutableStateFlow(Subtitle(emptyList()))
|
||||
override val subtitle: StateFlow<Subtitle> = subtitleState.asStateFlow()
|
||||
private val recordTitle = MutableStateFlow(Phrase(transcription = ""))
|
||||
private val subtitle = MutableStateFlow(Subtitle(emptyList()))
|
||||
|
||||
override val playbackState: StateFlow<PlaybackState> = audioPlayer.playbackState
|
||||
private val playbackState: StateFlow<PlaybackState> = audioPlayer.playbackState
|
||||
|
||||
override val duration: Duration get() = audioPlayer.duration
|
||||
private val duration: Duration get() = audioPlayer.duration
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val currentPosition: Flow<Duration> = playbackState.flatMapLatest { state ->
|
||||
private val currentPosition: Flow<Duration> = playbackState.flatMapLatest { state ->
|
||||
if (state == PlaybackState.PLAYING) {
|
||||
flow {
|
||||
while (true) {
|
||||
@@ -54,15 +55,15 @@ internal class PlaybackRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override val playbackTiming = currentPosition.map { currentPosition ->
|
||||
private val playbackTiming = currentPosition.map { currentPosition ->
|
||||
PlaybackTiming(
|
||||
positionMs = currentPosition.inWholeMilliseconds,
|
||||
durationMs = duration.inWholeMilliseconds,
|
||||
)
|
||||
}
|
||||
|
||||
override val currentSubtitleIndex: Flow<Int?> = combine(
|
||||
subtitleState,
|
||||
private val currentSubtitleIndex: Flow<Int?> = combine(
|
||||
subtitle,
|
||||
currentPosition,
|
||||
) { subtitle, position ->
|
||||
getCurrentSubtitleIndex(
|
||||
@@ -71,9 +72,26 @@ internal class PlaybackRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override val playerState: Flow<PlayerState> = combine(
|
||||
playbackState,
|
||||
subtitle,
|
||||
playbackTiming,
|
||||
currentSubtitleIndex,
|
||||
recordTitle,
|
||||
) { playbackState, subtitle, playbackTiming, currentSubtitleIndex, recordTitle ->
|
||||
PlayerState(
|
||||
playbackState = playbackState,
|
||||
playbackTiming = playbackTiming,
|
||||
subtitleLines = subtitle.lines,
|
||||
currentSubtitleIndex = currentSubtitleIndex,
|
||||
recordTitle = recordTitle,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun load(record: Record) {
|
||||
playAudio(filePath = record.audioResourcePath)
|
||||
subtitleState.value = loadSubtitle(record = record)
|
||||
subtitle.value = loadSubtitle(record = record)
|
||||
recordTitle.value = record.title
|
||||
}
|
||||
|
||||
private fun playAudio(filePath: String) {
|
||||
@@ -120,12 +138,12 @@ internal class PlaybackRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun seekToSentence(sentenceIndex: Int) {
|
||||
val matchingSubtitleLine = subtitleState.value.lines.getOrNull(sentenceIndex) ?: return
|
||||
val matchingSubtitleLine = subtitle.value.lines.getOrNull(sentenceIndex) ?: return
|
||||
audioPlayer.seekTo(matchingSubtitleLine.startTime)
|
||||
}
|
||||
|
||||
override fun goToPreviousSentence() {
|
||||
val lines = subtitleState.value.lines
|
||||
val lines = subtitle.value.lines
|
||||
val currentIndex = currentSentenceIndex() ?: return
|
||||
val currentLine = lines.getOrNull(currentIndex) ?: return
|
||||
val elapsedInSentence = audioPlayer.currentPosition - currentLine.startTime
|
||||
@@ -139,14 +157,14 @@ internal class PlaybackRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun goToNextSentence() {
|
||||
val lines = subtitleState.value.lines
|
||||
val lines = subtitle.value.lines
|
||||
val currentIndex = currentSentenceIndex() ?: return
|
||||
seekToSentence((currentIndex + 1).coerceAtMost(lines.lastIndex))
|
||||
}
|
||||
|
||||
private fun currentSentenceIndex(): Int? =
|
||||
getCurrentSubtitleIndex(
|
||||
subtitle = subtitleState.value,
|
||||
subtitle = subtitle.value,
|
||||
position = audioPlayer.currentPosition,
|
||||
)
|
||||
|
||||
|
||||
+2
-11
@@ -3,6 +3,7 @@ package fr.ajaury.gwenedeg.player.domain
|
||||
import fr.ajaury.gwenedeg.core.model.Record
|
||||
import fr.ajaury.gwenedeg.player.model.PlaybackState
|
||||
import fr.ajaury.gwenedeg.player.model.PlaybackTiming
|
||||
import fr.ajaury.gwenedeg.player.model.PlayerState
|
||||
import fr.ajaury.gwenedeg.subtitle.model.Subtitle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -16,17 +17,7 @@ import kotlin.time.Duration
|
||||
* Keeps audio and subtitle in sync.
|
||||
*/
|
||||
interface PlaybackRepository {
|
||||
val playbackState: StateFlow<PlaybackState>
|
||||
|
||||
val subtitle: StateFlow<Subtitle>
|
||||
|
||||
val currentPosition: Flow<Duration>
|
||||
|
||||
val currentSubtitleIndex: Flow<Int?>
|
||||
|
||||
val duration: Duration
|
||||
|
||||
val playbackTiming: Flow<PlaybackTiming>
|
||||
val playerState: Flow<PlayerState>
|
||||
|
||||
suspend fun load(record: Record)
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package fr.ajaury.gwenedeg.player.model
|
||||
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
|
||||
|
||||
data class PlayerState(
|
||||
val recordTitle: Phrase = Phrase(transcription = ""),
|
||||
val playbackState: PlaybackState = PlaybackState.IDLE,
|
||||
val playbackTiming: PlaybackTiming = PlaybackTiming(),
|
||||
val subtitleLines: List<SubtitleLine> = emptyList(),
|
||||
val currentSubtitleIndex: Int? = null,
|
||||
)
|
||||
@@ -25,6 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.player.model.PlaybackState
|
||||
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.subtitle.model.SubtitleLine
|
||||
@@ -77,12 +78,12 @@ fun PlayerScreen(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = uiState.recordTitle,
|
||||
text = uiState.recordTitle.transcription,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
uiState.recordTitleTranslation?.let { translation ->
|
||||
uiState.recordTitle.translation?.let { translation ->
|
||||
Text(
|
||||
text = translation,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
@@ -112,8 +113,8 @@ fun PlayerScreen(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SubtitleList(
|
||||
lines = uiState.subtitleLines,
|
||||
currentIndex = uiState.currentSubtitleIndex,
|
||||
lines = uiState.playerState.subtitleLines,
|
||||
currentIndex = uiState.playerState.currentSubtitleIndex,
|
||||
onSeek = onSeekToSentence,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
@@ -121,8 +122,8 @@ fun PlayerScreen(
|
||||
)
|
||||
|
||||
PlayerControl(
|
||||
playbackState = uiState.playbackState,
|
||||
playbackTiming = uiState.playbackTiming,
|
||||
playbackState = uiState.playerState.playbackState,
|
||||
playbackTiming = uiState.playerState.playbackTiming,
|
||||
onSeek = onSeekToTimePart,
|
||||
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
|
||||
onPreviousSentenceClicked = onPreviousSentenceClicked,
|
||||
@@ -163,12 +164,16 @@ private fun PlayerScreenPlayingPreview() {
|
||||
GwenedegTheme {
|
||||
PlayerScreen(
|
||||
uiState = PlayerUiState(
|
||||
recordTitle = "Demat",
|
||||
recordTitleTranslation = "Bonjour",
|
||||
playbackState = PlaybackState.PLAYING,
|
||||
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
|
||||
subtitleLines = previewSubtitleLines,
|
||||
currentSubtitleIndex = 1,
|
||||
recordTitle = Phrase(
|
||||
transcription = "Demat",
|
||||
translation = "Bonjour",
|
||||
),
|
||||
playerState = PlayerState(
|
||||
playbackState = PlaybackState.PLAYING,
|
||||
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
|
||||
subtitleLines = previewSubtitleLines,
|
||||
currentSubtitleIndex = 1,
|
||||
),
|
||||
),
|
||||
onMainPlayActionButtonClicked = {},
|
||||
onSeekToSentence = {},
|
||||
@@ -183,12 +188,16 @@ private fun PlayerScreenPausedPreview() {
|
||||
GwenedegTheme {
|
||||
PlayerScreen(
|
||||
uiState = PlayerUiState(
|
||||
recordTitle = "Demat",
|
||||
recordTitleTranslation = "Bonjour",
|
||||
playbackState = PlaybackState.PAUSED,
|
||||
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
|
||||
subtitleLines = previewSubtitleLines,
|
||||
currentSubtitleIndex = 1,
|
||||
recordTitle = Phrase(
|
||||
transcription = "Demat",
|
||||
translation = "Bonjour",
|
||||
),
|
||||
playerState = PlayerState(
|
||||
playbackState = PlaybackState.PAUSED,
|
||||
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
|
||||
subtitleLines = previewSubtitleLines,
|
||||
currentSubtitleIndex = 1,
|
||||
),
|
||||
),
|
||||
onMainPlayActionButtonClicked = {},
|
||||
onSeekToSentence = {},
|
||||
@@ -203,12 +212,16 @@ private fun PlayerScreenEndedPreview() {
|
||||
GwenedegTheme {
|
||||
PlayerScreen(
|
||||
uiState = PlayerUiState(
|
||||
recordTitle = "Demat",
|
||||
recordTitleTranslation = "Bonjour",
|
||||
playbackState = PlaybackState.ENDED,
|
||||
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
|
||||
subtitleLines = previewSubtitleLines,
|
||||
currentSubtitleIndex = 2,
|
||||
recordTitle = Phrase(
|
||||
transcription = "Demat",
|
||||
translation = "Bonjour",
|
||||
),
|
||||
playerState = PlayerState(
|
||||
playbackState = PlaybackState.ENDED,
|
||||
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
|
||||
subtitleLines = previewSubtitleLines,
|
||||
currentSubtitleIndex = 2,
|
||||
),
|
||||
),
|
||||
onMainPlayActionButtonClicked = {},
|
||||
onSeekToSentence = {},
|
||||
|
||||
+4
-9
@@ -1,14 +1,9 @@
|
||||
package fr.ajaury.gwenedeg.player.ui.viewmodel
|
||||
|
||||
import fr.ajaury.gwenedeg.player.model.PlaybackState
|
||||
import fr.ajaury.gwenedeg.player.model.PlaybackTiming
|
||||
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.player.model.PlayerState
|
||||
|
||||
data class PlayerUiState(
|
||||
val recordTitle: String = "",
|
||||
val recordTitleTranslation: String? = null,
|
||||
val playbackState: PlaybackState = PlaybackState.IDLE,
|
||||
val playbackTiming: PlaybackTiming = PlaybackTiming(),
|
||||
val subtitleLines: List<SubtitleLine> = emptyList(),
|
||||
val currentSubtitleIndex: Int? = null,
|
||||
val recordTitle: Phrase = Phrase(transcription = ""),
|
||||
val playerState: PlayerState = PlayerState(),
|
||||
)
|
||||
|
||||
+8
-14
@@ -3,6 +3,7 @@ package fr.ajaury.gwenedeg.player.ui.viewmodel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import fr.ajaury.gwenedeg.core.logging.domain.Logger
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.core.model.Record
|
||||
import fr.ajaury.gwenedeg.player.domain.AudioSessionManager
|
||||
import fr.ajaury.gwenedeg.player.domain.PlaybackRepository
|
||||
@@ -13,7 +14,6 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -30,18 +30,11 @@ class PlayerViewModel(
|
||||
|
||||
val uiState: StateFlow<PlayerUiState> = combine(
|
||||
record,
|
||||
playbackRepository.playbackState,
|
||||
playbackRepository.playbackTiming,
|
||||
playbackRepository.subtitle,
|
||||
playbackRepository.currentSubtitleIndex,
|
||||
) { record, playbackState, playbackTiming, subtitle, currentSubtitleIndex ->
|
||||
playbackRepository.playerState,
|
||||
) { record, playerState ->
|
||||
PlayerUiState(
|
||||
recordTitle = record?.title.orEmpty(),
|
||||
recordTitleTranslation = record?.titleTranslation,
|
||||
playbackState = playbackState,
|
||||
playbackTiming = playbackTiming,
|
||||
subtitleLines = subtitle.lines,
|
||||
currentSubtitleIndex = currentSubtitleIndex,
|
||||
recordTitle = record?.title ?: Phrase(transcription = ""),
|
||||
playerState = playerState,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
@@ -75,7 +68,7 @@ class PlayerViewModel(
|
||||
}
|
||||
|
||||
fun performMainPlayAction() {
|
||||
when (uiState.value.playbackState) {
|
||||
when (uiState.value.playerState.playbackState) {
|
||||
PlaybackState.PLAYING -> playbackRepository.pause()
|
||||
PlaybackState.ENDED -> playbackRepository.replay()
|
||||
PlaybackState.PAUSED, PlaybackState.IDLE -> playbackRepository.play()
|
||||
@@ -83,7 +76,8 @@ class PlayerViewModel(
|
||||
}
|
||||
|
||||
fun seekToTimePart(progress: Float) {
|
||||
val position = (progress.toDouble() * uiState.value.playbackTiming.durationMs).milliseconds
|
||||
val playbackTiming = uiState.value.playerState.playbackTiming
|
||||
val position = (progress.toDouble() * playbackTiming.durationMs).milliseconds
|
||||
playbackRepository.seekTo(position)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user