new: add adjustable subtitle text size in player and preferences UI

This commit is contained in:
2026-07-03 17:30:20 +02:00
parent f3ade9483d
commit b6e7a6a665
10 changed files with 159 additions and 17 deletions
@@ -72,6 +72,8 @@ fun PlayerScreen(
onNextSentenceClicked = viewModel::goToNextSentence,
onShowTranscriptionChange = viewModel::setShowTranscription,
onShowTranslationChange = viewModel::setShowTranslation,
onTextSizeDecrease = viewModel::decreaseSubtitleTextSize,
onTextSizeIncrease = viewModel::increaseSubtitleTextSize,
onSpeedChange = viewModel::setPlaybackSpeed,
onBackClicked = onBackClicked,
)
@@ -88,6 +90,8 @@ fun PlayerScreen(
onNextSentenceClicked: () -> Unit = {},
onShowTranscriptionChange: (Boolean) -> Unit = {},
onShowTranslationChange: (Boolean) -> Unit = {},
onTextSizeDecrease: () -> Unit = {},
onTextSizeIncrease: () -> Unit = {},
onSpeedChange: (Float) -> Unit = {},
onBackClicked: () -> Unit = {},
) {
@@ -184,6 +188,8 @@ fun PlayerScreen(
subtitlePreferences = uiState.subtitlePreferences,
onShowTranscriptionChange = onShowTranscriptionChange,
onShowTranslationChange = onShowTranslationChange,
onTextSizeDecrease = onTextSizeDecrease,
onTextSizeIncrease = onTextSizeIncrease,
onDismiss = { showPreferences = false },
)
}
@@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -27,9 +28,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import fr.ajaury.gwenedeg.core.model.Phrase
@@ -90,23 +93,34 @@ fun SubtitleList(
}
}
val density = LocalDensity.current
BoxWithConstraints(modifier = modifier) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
// Half-height padding so the first and last lines can reach the vertical center.
contentPadding = PaddingValues(vertical = maxHeight / 2),
// Half-height padding so the first and last lines can reach the vertical center. Captured here
// because `maxHeight` (BoxWithConstraintsScope) is not in scope inside the provider lambda.
val verticalPadding = maxHeight / 2
// Scale only the subtitle fonts by overriding the density's fontScale (dp layout is untouched).
CompositionLocalProvider(
LocalDensity provides Density(
density = density.density,
fontScale = density.fontScale * subtitlePreferences.textScale,
),
) {
itemsIndexed(lines) { index, line ->
val isCurrent = index == centeredIndex
Subtitle(
line = line,
isCurrent = isCurrent,
subtitlePreferences = subtitlePreferences,
onClick = { onSeek(index) },
)
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = PaddingValues(vertical = verticalPadding),
) {
itemsIndexed(lines) { index, line ->
val isCurrent = index == centeredIndex
Subtitle(
line = line,
isCurrent = isCurrent,
subtitlePreferences = subtitlePreferences,
onClick = { onSeek(index) },
)
}
}
}
}
@@ -9,6 +9,7 @@ import fr.ajaury.gwenedeg.player.domain.PlaybackRepository
import fr.ajaury.gwenedeg.player.model.PlaybackState
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
import fr.ajaury.gwenedeg.preferences.model.PlaybackPreferences
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
import fr.ajaury.gwenedeg.records.domain.RecordRepository
import fr.ajaury.gwenedeg.records.model.Record
import kotlinx.coroutines.flow.MutableStateFlow
@@ -125,6 +126,22 @@ class PlayerViewModel(
}
}
fun increaseSubtitleTextSize() {
val current = uiState.value.subtitlePreferences.textScale
setSubtitleTextScale(SubtitlePreferences.nextTextScale(current))
}
fun decreaseSubtitleTextSize() {
val current = uiState.value.subtitlePreferences.textScale
setSubtitleTextScale(SubtitlePreferences.previousTextScale(current))
}
private fun setSubtitleTextScale(scale: Float) {
viewModelScope.launch {
preferencesRepository.setSubtitleTextScale(scale)
}
}
fun stop() {
playbackRepository.stop()
}
@@ -2,27 +2,38 @@ package fr.ajaury.gwenedeg.preferences.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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_preferences_title
import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_text_size_decrease
import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_text_size_increase
import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_text_size_label
import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_transcription_description
import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_transcription_label
import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_translation_description
import fr.ajaury.gwenedeg.resources.generated.resources.subtitle_translation_label
import fr.ajaury.gwenedeg.theme.ChomBevTheme
import org.jetbrains.compose.resources.stringResource
import kotlin.math.roundToInt
/**
* Bottom sheet letting the user toggle the display of the Breton transcription (BZH) and the
@@ -34,6 +45,8 @@ fun SubtitlePreferencesBottomSheet(
subtitlePreferences: SubtitlePreferences,
onShowTranscriptionChange: (Boolean) -> Unit = {},
onShowTranslationChange: (Boolean) -> Unit = {},
onTextSizeDecrease: () -> Unit = {},
onTextSizeIncrease: () -> Unit = {},
onDismiss: () -> Unit = {},
sheetState: SheetState = rememberModalBottomSheetState(),
) {
@@ -45,6 +58,8 @@ fun SubtitlePreferencesBottomSheet(
subtitlePreferences = subtitlePreferences,
onShowTranscriptionChange = onShowTranscriptionChange,
onShowTranslationChange = onShowTranslationChange,
onTextSizeDecrease = onTextSizeDecrease,
onTextSizeIncrease = onTextSizeIncrease,
)
}
}
@@ -54,6 +69,8 @@ private fun SubtitlePreferencesContent(
subtitlePreferences: SubtitlePreferences,
onShowTranscriptionChange: (Boolean) -> Unit = {},
onShowTranslationChange: (Boolean) -> Unit = {},
onTextSizeDecrease: () -> Unit = {},
onTextSizeIncrease: () -> Unit = {},
) {
Column(
modifier = Modifier
@@ -78,9 +95,63 @@ private fun SubtitlePreferencesContent(
checked = subtitlePreferences.showTranslation,
onCheckedChange = onShowTranslationChange,
)
SubtitleTextSizeControl(
textScale = subtitlePreferences.textScale,
onDecrease = onTextSizeDecrease,
onIncrease = onTextSizeIncrease,
)
}
}
@Composable
private fun SubtitleTextSizeControl(
textScale: Float,
onDecrease: () -> Unit = {},
onIncrease: () -> Unit = {},
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(Res.string.subtitle_text_size_label),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
)
IconButton(
onClick = onDecrease,
enabled = textScale > SubtitlePreferences.TEXT_SCALE_STEPS.first(),
) {
Icon(
imageVector = Icons.Default.Remove,
contentDescription = stringResource(Res.string.subtitle_text_size_decrease),
)
}
Text(
text = formatTextScale(textScale),
style = MaterialTheme.typography.titleMedium,
)
IconButton(
onClick = onIncrease,
enabled = textScale < SubtitlePreferences.TEXT_SCALE_STEPS.last(),
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(Res.string.subtitle_text_size_increase),
)
}
}
}
/** Formats a text-scale multiplier as a short label, e.g. 1.0 -> "x1", 1.2 -> "x1.2". */
private fun formatTextScale(scale: Float): String {
val tenths = (scale * 10).roundToInt()
return if (tenths % 10 == 0) "x${tenths / 10}" else "x${tenths / 10}.${tenths % 10}"
}
@Preview
@Composable
private fun SubtitlePreferencesContentPreview() {