From 8dc1e8e71e451e2bc839b6bdaca8013bd67b08d4 Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Mon, 12 Oct 2026 14:13:27 +0200 Subject: [PATCH] refactor: improve accessibility title for player --- .../bzh/ajaury/chombev/RowDescription.kt | 23 ++++++++ .../ajaury/chombev/player/ui/PlayerScreen.kt | 52 +++++++++++-------- .../player/ui/viewmodel/PlayerUiState.kt | 10 ++-- .../player/ui/viewmodel/PlayerViewModel.kt | 4 +- .../ajaury/chombev/records/ui/RecordView.kt | 7 +-- 5 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/RowDescription.kt diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/RowDescription.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/RowDescription.kt new file mode 100644 index 0000000..195223b --- /dev/null +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/RowDescription.kt @@ -0,0 +1,23 @@ +package bzh.ajaury.chombev + +import androidx.compose.runtime.Composable +import bzh.ajaury.chombev.core.model.Phrase +import bzh.ajaury.chombev.core.model.Record +import bzh.ajaury.chombev.resources.generated.resources.Res +import bzh.ajaury.chombev.resources.generated.resources.records_chapter_number +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun rowDescription(record: Record): String = rowDescription(index = record.index, title = record.title) + +@Composable +internal fun rowDescription( + index: Int, + title: Phrase, +): String { + val chapterNumber = stringResource(Res.string.records_chapter_number, index) + val rowDescription = + listOfNotNull(chapterNumber, title.translation) + .joinToString(separator = ", ") + return rowDescription +} diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt index 46622c5..19eaa4d 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import bzh.ajaury.chombev.core.model.Phrase +import bzh.ajaury.chombev.core.model.Record import bzh.ajaury.chombev.player.model.PlaybackState import bzh.ajaury.chombev.player.model.PlaybackTiming import bzh.ajaury.chombev.player.model.PlayerState @@ -51,6 +52,7 @@ import bzh.ajaury.chombev.resources.generated.resources.player_back import bzh.ajaury.chombev.resources.generated.resources.player_back_action_label import bzh.ajaury.chombev.resources.generated.resources.settings_open_action_description import bzh.ajaury.chombev.resources.generated.resources.subtitle_preferences_action +import bzh.ajaury.chombev.rowDescription import bzh.ajaury.chombev.subtitle.model.SubtitleLine import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @@ -204,35 +206,38 @@ private fun PlayerTopBar( // Animate the title in as it loads, and between records: the record data isn't there on // the first frame, so fade + rise it into place once uiState carries it. AnimatedContent( - targetState = uiState.recordEmoji to uiState.recordTitle, + targetState = uiState.record, transitionSpec = { fadeIn(tween()) togetherWith fadeOut(tween()) }, label = "PlayerTitle", - ) { (emoji, title) -> + ) { record -> + // Same description as the record's row in the list; none until the record is loaded. + val description = + record?.index?.let { rowDescription(index = it, title = record.title) } Row( modifier = Modifier .traversalOrder(PlayerTraversalOrder.TITLE) .clearAndSetSemantics { - contentDescription = title.translation ?: title.transcription + description?.let { contentDescription = it } }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Text( - text = emoji, + text = record?.emoji.orEmpty(), style = MaterialTheme.typography.headlineSmall, ) Column { Text( - text = title.transcription, + text = record?.title?.transcription.orEmpty(), style = MaterialTheme.typography.headlineSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - title.translation?.let { translation -> + record?.title?.translation?.let { translation -> Text( text = translation, style = MaterialTheme.typography.titleSmall, @@ -317,17 +322,25 @@ private val previewSubtitleLines = ), ) +private val fakeRecord = Record( + id = 1, + index = 1, + emoji = "🙏", + title = Phrase( + transcription = "Demat", + translation = "Bonjour", + ), + audioResourcePath = "", + subtitleResourcePath = "", +) + @Preview @Composable private fun PlayerScreenPlayingPreview() { MaterialTheme { PlayerScreen( uiState = PlayerUiState( - recordEmoji = "🙏", - recordTitle = Phrase( - transcription = "Demat", - translation = "Bonjour", - ), + fakeRecord, playerState = PlayerState( playbackState = PlaybackState.PLAYING, playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds), @@ -348,14 +361,13 @@ private fun PlayerScreenPausedPreview() { MaterialTheme { PlayerScreen( uiState = PlayerUiState( - recordEmoji = "🙏", - recordTitle = Phrase( - transcription = "Demat", - translation = "Bonjour", - ), + record = fakeRecord, playerState = PlayerState( playbackState = PlaybackState.PAUSED, - playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds), + playbackTiming = PlaybackTiming( + position = 1.2.seconds, + duration = 5.seconds, + ), subtitleLines = previewSubtitleLines, currentSubtitleIndex = 1, ), @@ -373,11 +385,7 @@ private fun PlayerScreenEndedPreview() { MaterialTheme { PlayerScreen( uiState = PlayerUiState( - recordEmoji = "🙏", - recordTitle = Phrase( - transcription = "Demat", - translation = "Bonjour", - ), + record = fakeRecord, playerState = PlayerState( playbackState = PlaybackState.ENDED, playbackTiming = PlaybackTiming(position = 1.2.seconds, duration = 5.seconds), diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerUiState.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerUiState.kt index c411806..a426419 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerUiState.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerUiState.kt @@ -1,6 +1,6 @@ package bzh.ajaury.chombev.player.ui.viewmodel -import bzh.ajaury.chombev.core.model.Phrase +import bzh.ajaury.chombev.core.model.Record import bzh.ajaury.chombev.player.model.PlayerState import bzh.ajaury.chombev.preferences.model.PlaybackPreferences import bzh.ajaury.chombev.preferences.model.SubtitlePreferences @@ -8,8 +8,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds data class PlayerUiState( - val recordEmoji: String = "", - val recordTitle: Phrase = Phrase(transcription = ""), + val record: Record? = null, val playerState: PlayerState = PlayerState(), val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(), val playbackSpeed: Float = PlaybackPreferences.DEFAULT_SPEED, @@ -20,7 +19,10 @@ data class PlayerUiState( get() = playerState.playbackTiming.position > 2.seconds val canGoToNextSentence: Boolean - get() = (playerState.currentSubtitleIndex ?: Int.MAX_VALUE) < playerState.subtitleLines.lastIndex + get() = ( + playerState.currentSubtitleIndex + ?: Int.MAX_VALUE + ) < playerState.subtitleLines.lastIndex /** True when any playback setting differs from its default, used to highlight the button. */ val isPlaybackModified: Boolean diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerViewModel.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerViewModel.kt index bd2dddb..608e597 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerViewModel.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import bzh.ajaury.chombev.core.accessibility.domain.AccessibilityRepository import bzh.ajaury.chombev.core.logging.domain.Logger -import bzh.ajaury.chombev.core.model.Phrase import bzh.ajaury.chombev.core.model.Record import bzh.ajaury.chombev.player.domain.AudioSessionManager import bzh.ajaury.chombev.player.domain.PlaybackRepository @@ -42,8 +41,7 @@ class PlayerViewModel( preferencesRepository.playbackPreferences, ) { record, playerState, subtitlePreferences, playbackPreferences -> PlayerUiState( - recordEmoji = record?.emoji ?: "", - recordTitle = record?.title ?: Phrase(transcription = ""), + record = record, playerState = playerState, subtitlePreferences = subtitlePreferences, playbackSpeed = playbackPreferences.speed, diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/records/ui/RecordView.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/records/ui/RecordView.kt index 12b2fb2..987b07a 100644 --- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/records/ui/RecordView.kt +++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/records/ui/RecordView.kt @@ -21,7 +21,7 @@ import bzh.ajaury.chombev.core.model.Phrase import bzh.ajaury.chombev.core.model.Record import bzh.ajaury.chombev.resources.generated.resources.Res import bzh.ajaury.chombev.resources.generated.resources.records_chapter_action_description -import bzh.ajaury.chombev.resources.generated.resources.records_chapter_number +import bzh.ajaury.chombev.rowDescription import org.jetbrains.compose.resources.stringResource @Composable @@ -29,10 +29,7 @@ fun RecordView( record: Record, onClick: () -> Unit = {}, ) { - val chapterNumber = stringResource(Res.string.records_chapter_number, record.index) - val rowDescription = - listOfNotNull(chapterNumber, record.title.transcription, record.title.translation) - .joinToString(separator = ", ") + val rowDescription = rowDescription(record) Row( modifier =