diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt index 2ae983e..5a38fd2 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/App.kt @@ -47,25 +47,14 @@ fun App() { is Route.Records -> NavEntry(key) { RecordsScreen( onRecordClick = { record -> - backStack.add( - Route.Player( - title = record.title, - audioResourcePath = record.audioResourcePath, - subtitleResourcePath = record.subtitleResourcePath, - translationSubtitleResourcePath = - record.translationSubtitleResourcePath, - ), - ) + backStack.add(Route.Player(recordId = record.id)) }, ) } is Route.Player -> NavEntry(key) { PlayerScreen( - recordTitle = key.title, - audioResourcePath = key.audioResourcePath, - subtitleResourcePath = key.subtitleResourcePath, - translationSubtitleResourcePath = key.translationSubtitleResourcePath, + recordId = key.recordId, onBackClicked = { backStack.removeLastOrNull() }, ) } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/navigation/Route.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/navigation/Route.kt index 2144337..1d9138f 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/navigation/Route.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/navigation/Route.kt @@ -10,9 +10,6 @@ sealed interface Route : NavKey { @Serializable data class Player( - val title: String, - val audioResourcePath: String, - val subtitleResourcePath: String, - val translationSubtitleResourcePath: String? = null, + val recordId: Int, ) : Route } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/PlayerScreen.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/PlayerScreen.kt index d183202..32d5474 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/PlayerScreen.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/PlayerScreen.kt @@ -31,28 +31,20 @@ import org.koin.compose.viewmodel.koinViewModel @Composable fun PlayerScreen( - recordTitle: String, - audioResourcePath: String, - subtitleResourcePath: String, - translationSubtitleResourcePath: String? = null, + recordId: Int, onBackClicked: () -> Unit = {}, viewModel: PlayerViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - DisposableEffect(audioResourcePath, subtitleResourcePath, translationSubtitleResourcePath) { - viewModel.play(filePath = audioResourcePath) - viewModel.loadSubtitle( - resourcePath = subtitleResourcePath, - translationResourcePath = translationSubtitleResourcePath, - ) + DisposableEffect(recordId) { + viewModel.loadRecord(recordId = recordId) onDispose { viewModel.stop() } } PlayerScreen( - recordTitle = recordTitle, uiState = uiState, onMainPlayActionButtonClicked = viewModel::performMainPlayAction, onSeekToTimePart = viewModel::seekToTimePart, @@ -64,7 +56,6 @@ fun PlayerScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun PlayerScreen( - recordTitle: String, uiState: PlayerUiState, onMainPlayActionButtonClicked: () -> Unit = {}, onSeekToTimePart: (progress: Float) -> Unit = {}, @@ -76,7 +67,7 @@ fun PlayerScreen( TopAppBar( title = { Text( - text = recordTitle, + text = uiState.recordTitle, style = MaterialTheme.typography.headlineSmall, ) }, @@ -129,8 +120,8 @@ private val previewSubtitleLines = private fun PlayerScreenPlayingPreview() { GwenedegTheme { PlayerScreen( - recordTitle = "Demat", uiState = PlayerUiState( + recordTitle = "Demat", playbackState = PlaybackState.PLAYING, playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000), subtitleLines = previewSubtitleLines, @@ -148,8 +139,8 @@ private fun PlayerScreenPlayingPreview() { private fun PlayerScreenPausedPreview() { GwenedegTheme { PlayerScreen( - recordTitle = "Demat", uiState = PlayerUiState( + recordTitle = "Demat", playbackState = PlaybackState.PAUSED, playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000), subtitleLines = previewSubtitleLines, @@ -167,8 +158,8 @@ private fun PlayerScreenPausedPreview() { private fun PlayerScreenEndedPreview() { GwenedegTheme { PlayerScreen( - recordTitle = "Demat", uiState = PlayerUiState( + recordTitle = "Demat", playbackState = PlaybackState.ENDED, playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000), subtitleLines = previewSubtitleLines, diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerUiState.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerUiState.kt index d156e9c..512db19 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerUiState.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerUiState.kt @@ -5,6 +5,7 @@ import fr.ajaury.gwenedeg.player.model.PlaybackTiming import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine data class PlayerUiState( + val recordTitle: String = "", val playbackState: PlaybackState = PlaybackState.IDLE, val playbackTiming: PlaybackTiming = PlaybackTiming(), val subtitleLines: List = emptyList(), diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt index d0342a7..0ce4acf 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt @@ -7,6 +7,8 @@ import fr.ajaury.gwenedeg.player.domain.AudioPlayer import fr.ajaury.gwenedeg.player.domain.AudioSessionManager import fr.ajaury.gwenedeg.player.model.PlaybackState import fr.ajaury.gwenedeg.player.model.PlaybackTiming +import fr.ajaury.gwenedeg.records.domain.Record +import fr.ajaury.gwenedeg.records.domain.RecordRepository import fr.ajaury.gwenedeg.resourcereader.data.domain.ResourceReader import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase import fr.ajaury.gwenedeg.subtitle.domain.Subtitle @@ -28,6 +30,7 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds class PlayerViewModel( + private val recordRepository: RecordRepository, private val resourceReader: ResourceReader, private val audioPlayer: AudioPlayer, private val audioSessionManager: AudioSessionManager, @@ -35,10 +38,10 @@ class PlayerViewModel( private val getCurrentSubtitleIndex: GetCurrentSubtitleIndexUseCase, private val logger: Logger, ) : ViewModel() { - private var filePath: String? = null - private val playbackState = audioPlayer.playbackState + private val recordTitle = MutableStateFlow("") + private val subtitle = MutableStateFlow(Subtitle(emptyList())) @OptIn(ExperimentalCoroutinesApi::class) @@ -57,11 +60,13 @@ class PlayerViewModel( } val uiState: StateFlow = combine( + recordTitle, playbackState, currentPosition, subtitle, - ) { playbackState, currentPosition, subtitle -> + ) { recordTitle, playbackState, currentPosition, subtitle -> PlayerUiState( + recordTitle = recordTitle, playbackState = playbackState, playbackTiming = PlaybackTiming( positionMs = currentPosition.inWholeMilliseconds, @@ -83,9 +88,22 @@ class PlayerViewModel( audioSessionManager.activate() } - fun play(filePath: String) { + fun loadRecord(recordId: Int) { + viewModelScope.launch { + val record = recordRepository.getRecord(id = recordId) + if (record == null) { + logger.error(message = "Record not found: $recordId") + return@launch + } + + recordTitle.value = record.title + play(filePath = record.audioResourcePath) + loadSubtitle(record = record) + } + } + + private fun play(filePath: String) { try { - this.filePath = filePath audioPlayer.load(uri = Res.getUri(filePath)) audioPlayer.play() } catch (exception: Exception) { @@ -93,26 +111,21 @@ class PlayerViewModel( } } - fun loadSubtitle( - resourcePath: String, - translationResourcePath: String? = null, - ) { - viewModelScope.launch { - subtitle.value = try { - val content = resourceReader.read(resourcePath = resourcePath) - val translationContent = translationResourcePath - ?.let { resourceReader.read(resourcePath = it) } - subtitleRepository.getSubtitle( - content = content, - translationContent = translationContent, - ) - } catch (exception: Exception) { - logger.error( - message = "Failed to load subtitle: $resourcePath", - throwable = exception, - ) - Subtitle(emptyList()) - } + private suspend fun loadSubtitle(record: Record) { + subtitle.value = try { + val content = resourceReader.read(resourcePath = record.subtitleResourcePath) + val translationContent = record.translationSubtitleResourcePath + ?.let { resourceReader.read(resourcePath = it) } + subtitleRepository.getSubtitle( + content = content, + translationContent = translationContent, + ) + } catch (exception: Exception) { + logger.error( + message = "Failed to load subtitle: ${record.subtitleResourcePath}", + throwable = exception, + ) + Subtitle(emptyList()) } } @@ -124,7 +137,7 @@ class PlayerViewModel( } } - fun replay() { + private fun replay() { audioPlayer.seekTo(0.milliseconds) audioPlayer.play() } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/data/InMemoryRecordRepository.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/data/InMemoryRecordRepository.kt index 00cbb6e..b6a6007 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/data/InMemoryRecordRepository.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/data/InMemoryRecordRepository.kt @@ -35,6 +35,7 @@ class InMemoryRecordRepository : RecordRepository { val number = index + 1 val paddedNumber = number.toString().padStart(2, '0') Record( + id = number, index = number, title = title, audioResourcePath = "files/Chom_bev_$paddedNumber.m4a", @@ -44,4 +45,6 @@ class InMemoryRecordRepository : RecordRepository { } override fun getRecords(): Flow> = flowOf(records) + + override suspend fun getRecord(id: Int): Record? = records.firstOrNull { it.id == id } } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/domain/Record.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/domain/Record.kt index fa68e71..68f0eff 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/domain/Record.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/domain/Record.kt @@ -1,6 +1,7 @@ package fr.ajaury.gwenedeg.records.domain data class Record( + val id: Int, val index: Int, val title: String, val audioResourcePath: String, diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/domain/RecordRepository.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/domain/RecordRepository.kt index 6f2848e..9ad9c33 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/domain/RecordRepository.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/domain/RecordRepository.kt @@ -4,4 +4,6 @@ import kotlinx.coroutines.flow.Flow interface RecordRepository { fun getRecords(): Flow> + + suspend fun getRecord(id: Int): Record? } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/ui/RecordView.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/ui/RecordView.kt index 261ed22..dcf404e 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/ui/RecordView.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/ui/RecordView.kt @@ -42,6 +42,7 @@ private fun RecordViewPreview() { RecordView( record = Record( + id = 1, index = 1, title = "Sample Record", audioResourcePath = "files/Chom_bev_01.ogg", diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/ui/RecordsScreen.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/ui/RecordsScreen.kt index 548d1e3..97fdd6d 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/ui/RecordsScreen.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/records/ui/RecordsScreen.kt @@ -46,7 +46,7 @@ fun RecordsScreen( ) { itemsIndexed( items = records, - key = { _, item -> item.index }, + key = { _, item -> item.id }, ) { index, item -> RecordView( record = item,