refactor: use record IDs throughout app for better data handling and navigation
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
+39
-26
@@ -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,26 +111,21 @@ class PlayerViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadSubtitle(
|
private suspend fun loadSubtitle(record: Record) {
|
||||||
resourcePath: String,
|
subtitle.value = try {
|
||||||
translationResourcePath: String? = null,
|
val content = resourceReader.read(resourcePath = record.subtitleResourcePath)
|
||||||
) {
|
val translationContent = record.translationSubtitleResourcePath
|
||||||
viewModelScope.launch {
|
?.let { resourceReader.read(resourcePath = it) }
|
||||||
subtitle.value = try {
|
subtitleRepository.getSubtitle(
|
||||||
val content = resourceReader.read(resourcePath = resourcePath)
|
content = content,
|
||||||
val translationContent = translationResourcePath
|
translationContent = translationContent,
|
||||||
?.let { resourceReader.read(resourcePath = it) }
|
)
|
||||||
subtitleRepository.getSubtitle(
|
} catch (exception: Exception) {
|
||||||
content = content,
|
logger.error(
|
||||||
translationContent = translationContent,
|
message = "Failed to load subtitle: ${record.subtitleResourcePath}",
|
||||||
)
|
throwable = exception,
|
||||||
} catch (exception: Exception) {
|
)
|
||||||
logger.error(
|
Subtitle(emptyList())
|
||||||
message = "Failed to load subtitle: $resourcePath",
|
|
||||||
throwable = exception,
|
|
||||||
)
|
|
||||||
Subtitle(emptyList())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +137,7 @@ class PlayerViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replay() {
|
private fun replay() {
|
||||||
audioPlayer.seekTo(0.milliseconds)
|
audioPlayer.seekTo(0.milliseconds)
|
||||||
audioPlayer.play()
|
audioPlayer.play()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user