new: add subtitle translations

This commit is contained in:
2026-06-22 19:25:05 +02:00
parent feb3896727
commit ec9f927157
10 changed files with 129 additions and 28 deletions
@@ -52,6 +52,8 @@ fun App() {
title = record.title,
audioResourcePath = record.audioResourcePath,
subtitleResourcePath = record.subtitleResourcePath,
translationSubtitleResourcePath =
record.translationSubtitleResourcePath,
),
)
},
@@ -63,6 +65,7 @@ fun App() {
recordTitle = key.title,
audioResourcePath = key.audioResourcePath,
subtitleResourcePath = key.subtitleResourcePath,
translationSubtitleResourcePath = key.translationSubtitleResourcePath,
onBackClicked = { backStack.removeLastOrNull() },
)
}
@@ -13,5 +13,6 @@ sealed interface Route : NavKey {
val title: String,
val audioResourcePath: String,
val subtitleResourcePath: String,
val translationSubtitleResourcePath: String? = null,
) : Route
}
@@ -34,14 +34,18 @@ fun PlayerScreen(
recordTitle: String,
audioResourcePath: String,
subtitleResourcePath: String,
translationSubtitleResourcePath: String? = null,
onBackClicked: () -> Unit = {},
viewModel: PlayerViewModel = koinViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
DisposableEffect(audioResourcePath, subtitleResourcePath) {
DisposableEffect(audioResourcePath, subtitleResourcePath, translationSubtitleResourcePath) {
viewModel.play(filePath = audioResourcePath)
viewModel.loadSubtitle(resourcePath = subtitleResourcePath)
viewModel.loadSubtitle(
resourcePath = subtitleResourcePath,
translationResourcePath = translationSubtitleResourcePath,
)
onDispose {
viewModel.stop()
}
@@ -115,9 +119,9 @@ fun PlayerScreen(
private val previewSubtitleLines =
listOf(
SubtitleLine(startMs = 0, text = "Demat deoc'h !"),
SubtitleLine(startMs = 4310, text = "Kenavo !"),
SubtitleLine(startMs = 7350, text = "Kenavo emberr !"),
SubtitleLine(startMs = 0, text = "Demat deoc'h !", translation = "Bonjour !"),
SubtitleLine(startMs = 4310, text = "Kenavo !", translation = "Au revoir !"),
SubtitleLine(startMs = 7350, text = "Kenavo emberr !", translation = "À ce soir !"),
)
@Preview
@@ -4,6 +4,7 @@ import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -22,6 +23,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.dp
@@ -29,7 +31,6 @@ import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine
import fr.ajaury.gwenedeg.theme.GwenedegTheme
import kotlin.math.abs
@Composable
fun SubtitleList(
modifier: Modifier = Modifier,
@@ -47,7 +48,11 @@ fun SubtitleList(
-1
} else {
val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2f
info.visibleItemsInfo.minByOrNull { abs((it.offset + it.size / 2f) - viewportCenter) }!!.index
info.visibleItemsInfo
.minByOrNull {
abs((it.offset + it.size / 2f) - viewportCenter)
}?.index
?: -1
}
}
}
@@ -115,18 +120,69 @@ private fun Subtitle(
line: SubtitleLine,
isCurrent: Boolean,
) {
val contentColor = if (isCurrent) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
MainTextLine(
text = line.text,
isCurrent = isCurrent,
contentColor = contentColor,
)
line.translation?.let { translation ->
TranslationLine(
text = translation,
isCurrent = isCurrent,
contentColor = contentColor,
)
}
}
}
@Composable
private fun MainTextLine(
text: String,
isCurrent: Boolean,
contentColor: Color,
) {
val mainTextStyle = if (isCurrent) {
MaterialTheme.typography.headlineSmall
} else {
MaterialTheme.typography.bodyLarge
}
Text(
text = line.text,
style = if (isCurrent) {
MaterialTheme.typography.headlineSmall
} else {
MaterialTheme.typography.bodyLarge
},
color = if (isCurrent) {
Color.Unspecified
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
},
text = text,
style = mainTextStyle,
color = contentColor,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
@Composable
private fun TranslationLine(
text: String,
isCurrent: Boolean,
contentColor: Color,
) {
val translationStyle = if (isCurrent) {
MaterialTheme.typography.bodyLarge
} else {
MaterialTheme.typography.bodySmall
}
Text(
text = text,
style = translationStyle.copy(fontStyle = FontStyle.Italic),
color = contentColor,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
@@ -134,9 +190,9 @@ private fun Subtitle(
private val previewSubtitleLines =
listOf(
SubtitleLine(startMs = 0, text = "Demat deoc'h !"),
SubtitleLine(startMs = 4310, text = "Kenavo !"),
SubtitleLine(startMs = 7350, text = "Kenavo emberr !"),
SubtitleLine(startMs = 0, text = "Demat deoc'h !", translation = "Bonjour !"),
SubtitleLine(startMs = 4310, text = "Kenavo !", translation = "Au revoir !"),
SubtitleLine(startMs = 7350, text = "Kenavo emberr !", translation = "À ce soir !"),
)
@Preview
@@ -148,4 +204,4 @@ private fun SubtitleListPreview() {
currentIndex = 1,
)
}
}
}
@@ -93,11 +93,19 @@ class PlayerViewModel(
}
}
fun loadSubtitle(resourcePath: String) {
fun loadSubtitle(
resourcePath: String,
translationResourcePath: String? = null,
) {
viewModelScope.launch {
subtitle.value = try {
val content = resourceReader.read(resourcePath = resourcePath)
subtitleRepository.getSubtitle(content = content)
val translationContent = translationResourcePath
?.let { resourceReader.read(resourcePath = it) }
subtitleRepository.getSubtitle(
content = content,
translationContent = translationContent,
)
} catch (exception: Exception) {
logger.error(
message = "Failed to load subtitle: $resourcePath",
@@ -39,6 +39,7 @@ class InMemoryRecordRepository : RecordRepository {
title = title,
audioResourcePath = "files/Chom_bev_$paddedNumber.m4a",
subtitleResourcePath = "files/Chom_bev_${paddedNumber}_st_bzh.lrc",
translationSubtitleResourcePath = "files/Chom_bev_${paddedNumber}_st_fr.lrc",
)
}
@@ -5,4 +5,5 @@ data class Record(
val title: String,
val audioResourcePath: String,
val subtitleResourcePath: String,
val translationSubtitleResourcePath: String? = null,
)