refactor: use SSoT for playback speed

This commit is contained in:
2026-07-03 13:54:25 +02:00
parent d2283ae347
commit bd25dee1f0
4 changed files with 27 additions and 11 deletions
+2
View File
@@ -11,10 +11,12 @@ kotlin {
commonMain.dependencies { commonMain.dependencies {
// Core modules // Core modules
implementation(projects.core.audioplayer) implementation(projects.core.audioplayer)
implementation(projects.core.coroutines)
implementation(projects.core.logging) implementation(projects.core.logging)
implementation(projects.core.model) implementation(projects.core.model)
// Data modules // Data modules
implementation(projects.data.preferences)
implementation(projects.data.records) implementation(projects.data.records)
implementation(projects.data.resources) implementation(projects.data.resources)
implementation(projects.data.subtitle) implementation(projects.data.subtitle)
@@ -1,5 +1,6 @@
package fr.ajaury.gwenedeg.player.data package fr.ajaury.gwenedeg.player.data
import fr.ajaury.gwenedeg.core.coroutines.domain.DispatcherProvider
import fr.ajaury.gwenedeg.core.logging.domain.Logger import fr.ajaury.gwenedeg.core.logging.domain.Logger
import fr.ajaury.gwenedeg.core.model.Phrase import fr.ajaury.gwenedeg.core.model.Phrase
import fr.ajaury.gwenedeg.player.domain.AudioPlayer import fr.ajaury.gwenedeg.player.domain.AudioPlayer
@@ -7,12 +8,16 @@ import fr.ajaury.gwenedeg.player.domain.PlaybackRepository
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.player.model.PlayerState import fr.ajaury.gwenedeg.player.model.PlayerState
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
import fr.ajaury.gwenedeg.records.model.Record import fr.ajaury.gwenedeg.records.model.Record
import fr.ajaury.gwenedeg.resources.domain.ResourceReader import fr.ajaury.gwenedeg.resources.domain.ResourceReader
import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository
import fr.ajaury.gwenedeg.subtitle.model.Subtitle import fr.ajaury.gwenedeg.subtitle.model.Subtitle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -22,6 +27,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -30,9 +36,15 @@ internal class PlaybackRepositoryImpl(
private val audioPlayer: AudioPlayer, private val audioPlayer: AudioPlayer,
private val resourceReader: ResourceReader, private val resourceReader: ResourceReader,
private val subtitleRepository: SubtitleRepository, private val subtitleRepository: SubtitleRepository,
private val preferencesRepository: PreferencesRepository,
private val getCurrentSubtitleIndex: GetCurrentSubtitleIndexUseCase, private val getCurrentSubtitleIndex: GetCurrentSubtitleIndexUseCase,
private val dispatcherProvider: DispatcherProvider,
private val logger: Logger, private val logger: Logger,
) : PlaybackRepository { ) : PlaybackRepository {
// Audio operations must run on the main thread (e.g. ExoPlayer), so keep the scope on it.
private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.main)
private var speedObservationJob: Job? = null
private val recordTitle = MutableStateFlow(Phrase(transcription = "")) private val recordTitle = MutableStateFlow(Phrase(transcription = ""))
private val subtitle = MutableStateFlow(Subtitle(emptyList())) private val subtitle = MutableStateFlow(Subtitle(emptyList()))
@@ -91,6 +103,17 @@ internal class PlaybackRepositoryImpl(
playAudio(filePath = record.audioResourcePath) playAudio(filePath = record.audioResourcePath)
subtitle.value = loadSubtitle(record = record) subtitle.value = loadSubtitle(record = record)
recordTitle.value = record.title recordTitle.value = record.title
observePlaybackSpeed()
}
private fun observePlaybackSpeed() {
speedObservationJob?.cancel()
speedObservationJob = scope.launch {
preferencesRepository.playbackPreferences.collect { playbackPreferences ->
setSpeed(speed = playbackPreferences.speed)
}
}
} }
private fun playAudio(filePath: String) { private fun playAudio(filePath: String) {
@@ -136,7 +159,7 @@ internal class PlaybackRepositoryImpl(
audioPlayer.seekTo(position) audioPlayer.seekTo(position)
} }
override fun setSpeed(speed: Float) { private fun setSpeed(speed: Float) {
audioPlayer.setSpeed(speed) audioPlayer.setSpeed(speed)
} }
@@ -25,8 +25,6 @@ interface PlaybackRepository {
fun seekTo(position: Duration) fun seekTo(position: Duration)
fun setSpeed(speed: Float)
fun seekToSentence(sentenceIndex: Int) fun seekToSentence(sentenceIndex: Int)
fun goToPreviousSentence() fun goToPreviousSentence()
@@ -62,7 +62,6 @@ class PlayerViewModel(
if (alreadyLoadedRecord != null) { if (alreadyLoadedRecord != null) {
logger.debug("Record $recordId already loaded") logger.debug("Record $recordId already loaded")
playbackRepository.load(record = alreadyLoadedRecord) playbackRepository.load(record = alreadyLoadedRecord)
applyCurrentSpeed()
return@launch return@launch
} }
@@ -73,16 +72,10 @@ class PlayerViewModel(
} }
playbackRepository.load(record = loadedRecord) playbackRepository.load(record = loadedRecord)
applyCurrentSpeed()
record.value = loadedRecord record.value = loadedRecord
} }
} }
private suspend fun applyCurrentSpeed() {
val playbackPreferences = preferencesRepository.playbackPreferences.firstOrNull() ?: return
playbackRepository.setSpeed(playbackPreferences.speed)
}
fun performMainPlayAction() { fun performMainPlayAction() {
when (uiState.value.playerState.playbackState) { when (uiState.value.playerState.playbackState) {
PlaybackState.PLAYING -> playbackRepository.pause() PlaybackState.PLAYING -> playbackRepository.pause()
@@ -113,9 +106,9 @@ class PlayerViewModel(
PlaybackPreferences.MIN_SPEED, PlaybackPreferences.MIN_SPEED,
PlaybackPreferences.MAX_SPEED, PlaybackPreferences.MAX_SPEED,
) )
viewModelScope.launch { viewModelScope.launch {
preferencesRepository.setPlaybackSpeed(clampedSpeed) preferencesRepository.setPlaybackSpeed(clampedSpeed)
playbackRepository.setSpeed(clampedSpeed)
} }
} }