diff --git a/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/data/InMemoryPreferencesRepository.kt b/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/data/InMemoryPreferencesRepository.kt index 15d9830..cd58d3b 100644 --- a/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/data/InMemoryPreferencesRepository.kt +++ b/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/data/InMemoryPreferencesRepository.kt @@ -32,6 +32,10 @@ internal class InMemoryPreferencesRepository : PreferencesRepository { subtitleState.update { it.copy(textScale = scale) } } + override suspend fun setShowPhoneticMarkers(enabled: Boolean) { + subtitleState.update { it.copy(showPhoneticMarkers = enabled) } + } + override suspend fun setPlaybackSpeed(speed: Float) { playbackState.update { it.copy(speed = speed) } } diff --git a/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/domain/PreferencesRepository.kt b/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/domain/PreferencesRepository.kt index 1b537af..5c452b1 100644 --- a/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/domain/PreferencesRepository.kt +++ b/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/domain/PreferencesRepository.kt @@ -16,6 +16,8 @@ interface PreferencesRepository { suspend fun setSubtitleTextScale(scale: Float) + suspend fun setShowPhoneticMarkers(enabled: Boolean) + suspend fun setPlaybackSpeed(speed: Float) suspend fun setBreakBetweenPhrases(duration: Duration) diff --git a/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/model/SubtitlePreferences.kt b/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/model/SubtitlePreferences.kt index a63050c..4fa34ba 100644 --- a/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/model/SubtitlePreferences.kt +++ b/data/preferences/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/model/SubtitlePreferences.kt @@ -1,16 +1,23 @@ package fr.ajaury.gwenedeg.preferences.model +import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences.Companion.TEXT_SCALE_STEPS + + /** * User preferences controlling the subtitle display on the player. * * @property showTranscription whether the Breton (BZH) transcription line is shown. * @property showTranslation whether the French (FR) translation line is shown. * @property textScale multiplier applied to the subtitle font size (see [TEXT_SCALE_STEPS]). + * @property showPhoneticMarkers whether the muted `{x}` and liaison `[x]` letters are rendered with + * their pronunciation marks. When off, muted letters read as normal letters and liaison letters are + * replaced by a space. */ data class SubtitlePreferences( val showTranscription: Boolean = true, val showTranslation: Boolean = true, val textScale: Float = DEFAULT_TEXT_SCALE, + val showPhoneticMarkers: Boolean = true, ) { companion object { const val DEFAULT_TEXT_SCALE = 1f @@ -20,11 +27,9 @@ data class SubtitlePreferences( listOf(0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f, 1.2f, 1.4f, 1.6f, 1.8f, 2.0f) /** The next allowed step above [scale], or the maximum when already at the top. */ - fun nextTextScale(scale: Float): Float = - TEXT_SCALE_STEPS.firstOrNull { it > scale } ?: TEXT_SCALE_STEPS.last() + fun nextTextScale(scale: Float): Float = TEXT_SCALE_STEPS.firstOrNull { it > scale } ?: TEXT_SCALE_STEPS.last() /** The previous allowed step below [scale], or the minimum when already at the bottom. */ - fun previousTextScale(scale: Float): Float = - TEXT_SCALE_STEPS.lastOrNull { it < scale } ?: TEXT_SCALE_STEPS.first() + fun previousTextScale(scale: Float): Float = TEXT_SCALE_STEPS.lastOrNull { it < scale } ?: TEXT_SCALE_STEPS.first() } } diff --git a/data/preferences/src/dataStoreMain/kotlin/fr/ajaury/gwenedeg/preferences/data/DataStorePreferencesRepository.kt b/data/preferences/src/dataStoreMain/kotlin/fr/ajaury/gwenedeg/preferences/data/DataStorePreferencesRepository.kt index c805e99..c328fa8 100644 --- a/data/preferences/src/dataStoreMain/kotlin/fr/ajaury/gwenedeg/preferences/data/DataStorePreferencesRepository.kt +++ b/data/preferences/src/dataStoreMain/kotlin/fr/ajaury/gwenedeg/preferences/data/DataStorePreferencesRepository.kt @@ -37,6 +37,8 @@ internal class DataStorePreferencesRepository( override suspend fun setSubtitleTextScale(scale: Float) = edit { it[SubtitleTextScaleKey] = scale } + override suspend fun setShowPhoneticMarkers(enabled: Boolean) = edit { it[ShowPhoneticMarkersKey] = enabled } + override suspend fun setPlaybackSpeed(speed: Float) = edit { it[PlaybackSpeedKey] = speed } override suspend fun setBreakBetweenPhrases(duration: Duration) = @@ -52,6 +54,7 @@ internal class DataStorePreferencesRepository( showTranscription = this[ShowTranscriptionKey] ?: defaults.showTranscription, showTranslation = this[ShowTranslationKey] ?: defaults.showTranslation, textScale = this[SubtitleTextScaleKey] ?: defaults.textScale, + showPhoneticMarkers = this[ShowPhoneticMarkersKey] ?: defaults.showPhoneticMarkers, ) } @@ -66,6 +69,7 @@ internal class DataStorePreferencesRepository( val ShowTranscriptionKey = booleanPreferencesKey("show_transcription") val ShowTranslationKey = booleanPreferencesKey("show_translation") val SubtitleTextScaleKey = floatPreferencesKey("subtitle_text_scale") + val ShowPhoneticMarkersKey = booleanPreferencesKey("show_phonetic_marker") val PlaybackSpeedKey = floatPreferencesKey("playback_speed") val BreakBetweenPhrasesSecondsKey = intPreferencesKey("break_between_phrases_seconds") } diff --git a/data/resources/src/commonMain/composeResources/values-br/strings.xml b/data/resources/src/commonMain/composeResources/values-br/strings.xml index acb42e8..f7c8a31 100644 --- a/data/resources/src/commonMain/composeResources/values-br/strings.xml +++ b/data/resources/src/commonMain/composeResources/values-br/strings.xml @@ -11,6 +11,8 @@ Ment an destenn Bihanaat ment an destenn Brasaat ment an destenn + Merkoù distagañ + Diskouez ar c\'hensonennoù mut ha al liammoù Tizh al lenn Arventennoù al lenn diff --git a/data/resources/src/commonMain/composeResources/values/strings.xml b/data/resources/src/commonMain/composeResources/values/strings.xml index 001ec5b..9102b6f 100644 --- a/data/resources/src/commonMain/composeResources/values/strings.xml +++ b/data/resources/src/commonMain/composeResources/values/strings.xml @@ -11,6 +11,8 @@ Taille du texte Réduire la taille du texte Augmenter la taille du texte + Marques de prononciation + Afficher les consonnes muettes et les liaisons Vitesse de lecture Réglages de lecture 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 31ecd2a..41f1a0b 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 @@ -72,6 +72,7 @@ fun PlayerScreen( onNextSentenceClicked = viewModel::goToNextSentence, onShowTranscriptionChange = viewModel::setShowTranscription, onShowTranslationChange = viewModel::setShowTranslation, + onShowPhoneticMarkerChange = viewModel::setShowPhoneticMarkers, onTextSizeDecrease = viewModel::decreaseSubtitleTextSize, onTextSizeIncrease = viewModel::increaseSubtitleTextSize, onSpeedChange = viewModel::setPlaybackSpeed, @@ -91,6 +92,7 @@ fun PlayerScreen( onNextSentenceClicked: () -> Unit = {}, onShowTranscriptionChange: (Boolean) -> Unit = {}, onShowTranslationChange: (Boolean) -> Unit = {}, + onShowPhoneticMarkerChange: (Boolean) -> Unit = {}, onTextSizeDecrease: () -> Unit = {}, onTextSizeIncrease: () -> Unit = {}, onSpeedChange: (Float) -> Unit = {}, @@ -190,6 +192,7 @@ fun PlayerScreen( subtitlePreferences = uiState.subtitlePreferences, onShowTranscriptionChange = onShowTranscriptionChange, onShowTranslationChange = onShowTranslationChange, + onShowPhoneticMarkerChange = onShowPhoneticMarkerChange, onTextSizeDecrease = onTextSizeDecrease, onTextSizeIncrease = onTextSizeIncrease, onDismiss = { showPreferences = false }, diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/SubtitleList.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/SubtitleList.kt index 8b861a0..0c1ccef 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/SubtitleList.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/SubtitleList.kt @@ -177,6 +177,7 @@ private fun Subtitle( TranscriptionLine( text = line.phrase.transcription, contentColor = contentColor, + showPhoneticMarkers = subtitlePreferences.showPhoneticMarkers, ) } line.phrase.translation @@ -194,6 +195,7 @@ private fun Subtitle( private fun TranscriptionLine( text: String, contentColor: Color, + showPhoneticMarkers: Boolean = true, ) { TranscriptionText( text = text, @@ -203,6 +205,7 @@ private fun TranscriptionLine( lineHeight = 32.sp, ), textAlign = TextAlign.Center, + showPhoneticMarkers = showPhoneticMarkers, ) } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/components/TranscriptionText.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/components/TranscriptionText.kt index 5154a84..af5f0b0 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/components/TranscriptionText.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/components/TranscriptionText.kt @@ -38,7 +38,7 @@ import fr.ajaury.gwenedeg.theme.ChomBevTheme * * The captured group is the bare letter, without its surrounding delimiters. */ -private val MARKED_LETTER_REGEX = Regex("""\(([A-Za-z])\)|\[([A-Za-z])\]""") +private val MARKED_LETTER_REGEX = Regex("""\{([A-Za-z])\}|\[([A-Za-z])\]""") /** Diacritic drawn above a parenthesised letter to mark that it is muted. */ private const val MUTED_MARK = "ø" @@ -51,12 +51,15 @@ private const val WORD_JOINER = "\u2060" /** * Renders a transcription, giving special treatment to single letters written between delimiters: - * - `(x)` — a liaison consonant: the parentheses are dropped and the letter stays on the line, in + * - `{x}` — a liaison consonant: the parentheses are dropped and the letter stays on the line, in * muted grey, topped with a small [MUTED_MARK]. * - `[x]` — a linking consonant: the brackets are dropped and the letter is raised above the line, * in muted grey, with a small [LIAISON_MARK] underneath it. * * Marked letters are emitted as inline content so the line still wraps like normal text. + * + * When [showPhoneticMarkers] is `false` the pronunciation marks are dropped entirely: a muted + * curved-bracketed letter reads as a normal letter, and a bracketed liaison letter becomes a space. */ @Composable fun TranscriptionText( @@ -65,12 +68,18 @@ fun TranscriptionText( style: TextStyle, modifier: Modifier = Modifier, textAlign: TextAlign = TextAlign.Center, + showPhoneticMarkers: Boolean = true, ) { - val matches = remember(text) { MARKED_LETTER_REGEX.findAll(text).toList() } + val matches = remember(text, showPhoneticMarkers) { + if (showPhoneticMarkers) MARKED_LETTER_REGEX.findAll(text).toList() else emptyList() + } if (matches.isEmpty()) { + val plainText = remember(text, showPhoneticMarkers) { + if (showPhoneticMarkers) text else text.withoutMarkedLetters() + } Text( - text = text, + text = plainText, color = color, style = style, textAlign = textAlign, @@ -211,6 +220,24 @@ private val MatchResult.letter: String private val MatchResult.isBracketed: Boolean get() = groupValues[1].isEmpty() +/** + * Strips pronunciation marks from a transcription: a muted parenthesised letter becomes a plain + * letter, while a bracketed liaison letter is dropped and replaced by a space — except when it + * directly follows an apostrophe (e.g. `d'[t]ober`), where it is removed without leaving a space. + */ +private fun String.withoutMarkedLetters(): String = + replace(MARKED_LETTER_REGEX) { match -> + val precedingChar = getOrNull(match.range.first - 1) + when { + !match.isBracketed -> match.letter + precedingChar != null && precedingChar in APOSTROPHES -> "" + else -> " " + } + } + +/** Apostrophe characters after which a dropped liaison letter should not leave a space. */ +private val APOSTROPHES = setOf('\'', '’') + private fun inlineId(index: Int): String = "marked-letter-$index" @Preview(backgroundColor = 0xFFFFFFFF) 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 617a5d4..db4ed2e 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 @@ -17,7 +17,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -147,6 +146,12 @@ class PlayerViewModel( } } + fun setShowPhoneticMarkers(enabled: Boolean) { + viewModelScope.launch { + preferencesRepository.setShowPhoneticMarkers(enabled) + } + } + fun increaseSubtitleTextSize() { val current = uiState.value.subtitlePreferences.textScale setSubtitleTextScale(SubtitlePreferences.nextTextScale(current)) diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/ui/SubtitlePreferencesBottomSheet.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/ui/SubtitlePreferencesBottomSheet.kt index 2d064b5..cf498b5 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/ui/SubtitlePreferencesBottomSheet.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/preferences/ui/SubtitlePreferencesBottomSheet.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences import fr.ajaury.gwenedeg.resources.generated.resources.Res +import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_marked_letters_description +import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_marked_letters_label import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_preferences_title import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_text_size_decrease import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_text_size_increase @@ -45,6 +47,7 @@ fun SubtitlePreferencesBottomSheet( subtitlePreferences: SubtitlePreferences, onShowTranscriptionChange: (Boolean) -> Unit = {}, onShowTranslationChange: (Boolean) -> Unit = {}, + onShowPhoneticMarkerChange: (Boolean) -> Unit = {}, onTextSizeDecrease: () -> Unit = {}, onTextSizeIncrease: () -> Unit = {}, onDismiss: () -> Unit = {}, @@ -58,6 +61,7 @@ fun SubtitlePreferencesBottomSheet( subtitlePreferences = subtitlePreferences, onShowTranscriptionChange = onShowTranscriptionChange, onShowTranslationChange = onShowTranslationChange, + onShowMarkedLettersChange = onShowPhoneticMarkerChange, onTextSizeDecrease = onTextSizeDecrease, onTextSizeIncrease = onTextSizeIncrease, ) @@ -69,6 +73,7 @@ private fun SubtitlePreferencesContent( subtitlePreferences: SubtitlePreferences, onShowTranscriptionChange: (Boolean) -> Unit = {}, onShowTranslationChange: (Boolean) -> Unit = {}, + onShowMarkedLettersChange: (Boolean) -> Unit = {}, onTextSizeDecrease: () -> Unit = {}, onTextSizeIncrease: () -> Unit = {}, ) { @@ -95,6 +100,12 @@ private fun SubtitlePreferencesContent( checked = subtitlePreferences.showTranslation, onCheckedChange = onShowTranslationChange, ) + PreferenceToggle( + title = stringResource(Res.string.subtitle_marked_letters_label), + subtitle = stringResource(Res.string.subtitle_marked_letters_description), + checked = subtitlePreferences.showPhoneticMarkers, + onCheckedChange = onShowMarkedLettersChange, + ) SubtitleTextSizeControl( textScale = subtitlePreferences.textScale, onDecrease = onTextSizeDecrease,