refactor: use record IDs throughout app for better data handling and navigation

This commit is contained in:
2026-06-23 13:38:14 +02:00
parent ec9f927157
commit f8ed215921
10 changed files with 58 additions and 60 deletions
@@ -47,25 +47,14 @@ fun App() {
is Route.Records -> NavEntry(key) { is Route.Records -> NavEntry(key) {
RecordsScreen( RecordsScreen(
onRecordClick = { record -> onRecordClick = { record ->
backStack.add( backStack.add(Route.Player(recordId = record.id))
Route.Player(
title = record.title,
audioResourcePath = record.audioResourcePath,
subtitleResourcePath = record.subtitleResourcePath,
translationSubtitleResourcePath =
record.translationSubtitleResourcePath,
),
)
}, },
) )
} }
is Route.Player -> NavEntry(key) { is Route.Player -> NavEntry(key) {
PlayerScreen( PlayerScreen(
recordTitle = key.title, recordId = key.recordId,
audioResourcePath = key.audioResourcePath,
subtitleResourcePath = key.subtitleResourcePath,
translationSubtitleResourcePath = key.translationSubtitleResourcePath,
onBackClicked = { backStack.removeLastOrNull() }, onBackClicked = { backStack.removeLastOrNull() },
) )
} }
@@ -10,9 +10,6 @@ sealed interface Route : NavKey {
@Serializable @Serializable
data class Player( data class Player(
val title: String, val recordId: Int,
val audioResourcePath: String,
val subtitleResourcePath: String,
val translationSubtitleResourcePath: String? = null,
) : Route ) : Route
} }
@@ -31,28 +31,20 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable @Composable
fun PlayerScreen( fun PlayerScreen(
recordTitle: String, recordId: Int,
audioResourcePath: String,
subtitleResourcePath: String,
translationSubtitleResourcePath: String? = null,
onBackClicked: () -> Unit = {}, onBackClicked: () -> Unit = {},
viewModel: PlayerViewModel = koinViewModel(), viewModel: PlayerViewModel = koinViewModel(),
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
DisposableEffect(audioResourcePath, subtitleResourcePath, translationSubtitleResourcePath) { DisposableEffect(recordId) {
viewModel.play(filePath = audioResourcePath) viewModel.loadRecord(recordId = recordId)
viewModel.loadSubtitle(
resourcePath = subtitleResourcePath,
translationResourcePath = translationSubtitleResourcePath,
)
onDispose { onDispose {
viewModel.stop() viewModel.stop()
} }
} }
PlayerScreen( PlayerScreen(
recordTitle = recordTitle,
uiState = uiState, uiState = uiState,
onMainPlayActionButtonClicked = viewModel::performMainPlayAction, onMainPlayActionButtonClicked = viewModel::performMainPlayAction,
onSeekToTimePart = viewModel::seekToTimePart, onSeekToTimePart = viewModel::seekToTimePart,
@@ -64,7 +56,6 @@ fun PlayerScreen(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PlayerScreen( fun PlayerScreen(
recordTitle: String,
uiState: PlayerUiState, uiState: PlayerUiState,
onMainPlayActionButtonClicked: () -> Unit = {}, onMainPlayActionButtonClicked: () -> Unit = {},
onSeekToTimePart: (progress: Float) -> Unit = {}, onSeekToTimePart: (progress: Float) -> Unit = {},
@@ -76,7 +67,7 @@ fun PlayerScreen(
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
text = recordTitle, text = uiState.recordTitle,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
) )
}, },
@@ -129,8 +120,8 @@ private val previewSubtitleLines =
private fun PlayerScreenPlayingPreview() { private fun PlayerScreenPlayingPreview() {
GwenedegTheme { GwenedegTheme {
PlayerScreen( PlayerScreen(
recordTitle = "Demat",
uiState = PlayerUiState( uiState = PlayerUiState(
recordTitle = "Demat",
playbackState = PlaybackState.PLAYING, playbackState = PlaybackState.PLAYING,
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000), playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
subtitleLines = previewSubtitleLines, subtitleLines = previewSubtitleLines,
@@ -148,8 +139,8 @@ private fun PlayerScreenPlayingPreview() {
private fun PlayerScreenPausedPreview() { private fun PlayerScreenPausedPreview() {
GwenedegTheme { GwenedegTheme {
PlayerScreen( PlayerScreen(
recordTitle = "Demat",
uiState = PlayerUiState( uiState = PlayerUiState(
recordTitle = "Demat",
playbackState = PlaybackState.PAUSED, playbackState = PlaybackState.PAUSED,
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000), playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
subtitleLines = previewSubtitleLines, subtitleLines = previewSubtitleLines,
@@ -167,8 +158,8 @@ private fun PlayerScreenPausedPreview() {
private fun PlayerScreenEndedPreview() { private fun PlayerScreenEndedPreview() {
GwenedegTheme { GwenedegTheme {
PlayerScreen( PlayerScreen(
recordTitle = "Demat",
uiState = PlayerUiState( uiState = PlayerUiState(
recordTitle = "Demat",
playbackState = PlaybackState.ENDED, playbackState = PlaybackState.ENDED,
playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000), playbackTiming = PlaybackTiming(positionMs = 1234, durationMs = 5000),
subtitleLines = previewSubtitleLines, subtitleLines = previewSubtitleLines,
@@ -5,6 +5,7 @@ import fr.ajaury.gwenedeg.player.model.PlaybackTiming
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine
data class PlayerUiState( data class PlayerUiState(
val recordTitle: String = "",
val playbackState: PlaybackState = PlaybackState.IDLE, val playbackState: PlaybackState = PlaybackState.IDLE,
val playbackTiming: PlaybackTiming = PlaybackTiming(), val playbackTiming: PlaybackTiming = PlaybackTiming(),
val subtitleLines: List<SubtitleLine> = emptyList(), val subtitleLines: List<SubtitleLine> = emptyList(),
@@ -7,6 +7,8 @@ import fr.ajaury.gwenedeg.player.domain.AudioPlayer
import fr.ajaury.gwenedeg.player.domain.AudioSessionManager import fr.ajaury.gwenedeg.player.domain.AudioSessionManager
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.records.domain.Record
import fr.ajaury.gwenedeg.records.domain.RecordRepository
import fr.ajaury.gwenedeg.resourcereader.data.domain.ResourceReader import fr.ajaury.gwenedeg.resourcereader.data.domain.ResourceReader
import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase
import fr.ajaury.gwenedeg.subtitle.domain.Subtitle import fr.ajaury.gwenedeg.subtitle.domain.Subtitle
@@ -28,6 +30,7 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class PlayerViewModel( class PlayerViewModel(
private val recordRepository: RecordRepository,
private val resourceReader: ResourceReader, private val resourceReader: ResourceReader,
private val audioPlayer: AudioPlayer, private val audioPlayer: AudioPlayer,
private val audioSessionManager: AudioSessionManager, private val audioSessionManager: AudioSessionManager,
@@ -35,10 +38,10 @@ class PlayerViewModel(
private val getCurrentSubtitleIndex: GetCurrentSubtitleIndexUseCase, private val getCurrentSubtitleIndex: GetCurrentSubtitleIndexUseCase,
private val logger: Logger, private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
private var filePath: String? = null
private val playbackState = audioPlayer.playbackState private val playbackState = audioPlayer.playbackState
private val recordTitle = MutableStateFlow("")
private val subtitle = MutableStateFlow(Subtitle(emptyList())) private val subtitle = MutableStateFlow(Subtitle(emptyList()))
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@@ -57,11 +60,13 @@ class PlayerViewModel(
} }
val uiState: StateFlow<PlayerUiState> = combine( val uiState: StateFlow<PlayerUiState> = combine(
recordTitle,
playbackState, playbackState,
currentPosition, currentPosition,
subtitle, subtitle,
) { playbackState, currentPosition, subtitle -> ) { recordTitle, playbackState, currentPosition, subtitle ->
PlayerUiState( PlayerUiState(
recordTitle = recordTitle,
playbackState = playbackState, playbackState = playbackState,
playbackTiming = PlaybackTiming( playbackTiming = PlaybackTiming(
positionMs = currentPosition.inWholeMilliseconds, positionMs = currentPosition.inWholeMilliseconds,
@@ -83,9 +88,22 @@ class PlayerViewModel(
audioSessionManager.activate() 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 { try {
this.filePath = filePath
audioPlayer.load(uri = Res.getUri(filePath)) audioPlayer.load(uri = Res.getUri(filePath))
audioPlayer.play() audioPlayer.play()
} catch (exception: Exception) { } catch (exception: Exception) {
@@ -93,14 +111,10 @@ class PlayerViewModel(
} }
} }
fun loadSubtitle( private suspend fun loadSubtitle(record: Record) {
resourcePath: String,
translationResourcePath: String? = null,
) {
viewModelScope.launch {
subtitle.value = try { subtitle.value = try {
val content = resourceReader.read(resourcePath = resourcePath) val content = resourceReader.read(resourcePath = record.subtitleResourcePath)
val translationContent = translationResourcePath val translationContent = record.translationSubtitleResourcePath
?.let { resourceReader.read(resourcePath = it) } ?.let { resourceReader.read(resourcePath = it) }
subtitleRepository.getSubtitle( subtitleRepository.getSubtitle(
content = content, content = content,
@@ -108,13 +122,12 @@ class PlayerViewModel(
) )
} catch (exception: Exception) { } catch (exception: Exception) {
logger.error( logger.error(
message = "Failed to load subtitle: $resourcePath", message = "Failed to load subtitle: ${record.subtitleResourcePath}",
throwable = exception, throwable = exception,
) )
Subtitle(emptyList()) Subtitle(emptyList())
} }
} }
}
fun performMainPlayAction() { fun performMainPlayAction() {
when (uiState.value.playbackState) { when (uiState.value.playbackState) {
@@ -124,7 +137,7 @@ class PlayerViewModel(
} }
} }
fun replay() { private fun replay() {
audioPlayer.seekTo(0.milliseconds) audioPlayer.seekTo(0.milliseconds)
audioPlayer.play() audioPlayer.play()
} }
@@ -35,6 +35,7 @@ class InMemoryRecordRepository : RecordRepository {
val number = index + 1 val number = index + 1
val paddedNumber = number.toString().padStart(2, '0') val paddedNumber = number.toString().padStart(2, '0')
Record( Record(
id = number,
index = number, index = number,
title = title, title = title,
audioResourcePath = "files/Chom_bev_$paddedNumber.m4a", audioResourcePath = "files/Chom_bev_$paddedNumber.m4a",
@@ -44,4 +45,6 @@ class InMemoryRecordRepository : RecordRepository {
} }
override fun getRecords(): Flow<List<Record>> = flowOf(records) override fun getRecords(): Flow<List<Record>> = flowOf(records)
override suspend fun getRecord(id: Int): Record? = records.firstOrNull { it.id == id }
} }
@@ -1,6 +1,7 @@
package fr.ajaury.gwenedeg.records.domain package fr.ajaury.gwenedeg.records.domain
data class Record( data class Record(
val id: Int,
val index: Int, val index: Int,
val title: String, val title: String,
val audioResourcePath: String, val audioResourcePath: String,
@@ -4,4 +4,6 @@ import kotlinx.coroutines.flow.Flow
interface RecordRepository { interface RecordRepository {
fun getRecords(): Flow<List<Record>> fun getRecords(): Flow<List<Record>>
suspend fun getRecord(id: Int): Record?
} }
@@ -42,6 +42,7 @@ private fun RecordViewPreview() {
RecordView( RecordView(
record = record =
Record( Record(
id = 1,
index = 1, index = 1,
title = "Sample Record", title = "Sample Record",
audioResourcePath = "files/Chom_bev_01.ogg", audioResourcePath = "files/Chom_bev_01.ogg",
@@ -46,7 +46,7 @@ fun RecordsScreen(
) { ) {
itemsIndexed( itemsIndexed(
items = records, items = records,
key = { _, item -> item.index }, key = { _, item -> item.id },
) { index, item -> ) { index, item ->
RecordView( RecordView(
record = item, record = item,