new: add phonetic marker toggle to subtitles preferences

This commit is contained in:
2026-07-06 16:44:18 +02:00
parent 9dc5d27c19
commit 743697202e
11 changed files with 77 additions and 9 deletions
@@ -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 },
@@ -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,
)
}
@@ -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)
@@ -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))
@@ -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,