From c4572eb0421c499e2f930c428e657cfeb8f8c886 Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Fri, 19 Jun 2026 17:27:31 +0200 Subject: [PATCH] new: allow scrolling on subtitle list and advance playback --- .../kotlin/fr/ajaury/gwenedeg/di/Modules.kt | 4 +- .../ajaury/gwenedeg/player/ui/PlayerScreen.kt | 205 ++++++++++++------ .../player/ui/viewmodel/PlayerUiState.kt | 5 +- .../player/ui/viewmodel/PlayerViewModel.kt | 14 +- .../domain/GetCurrentSubtitleIndexUseCase.kt | 8 + .../domain/GetSubtitleWindowUseCase.kt | 16 -- .../subtitle/domain/SubtitleWindow.kt | 7 - .../GetCurrentSubtitleIndexUseCaseTest.kt | 44 ++++ .../subtitle/GetSubtitleWindowUseCaseTest.kt | 62 ------ 9 files changed, 202 insertions(+), 163 deletions(-) create mode 100644 shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt delete mode 100644 shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetSubtitleWindowUseCase.kt delete mode 100644 shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleWindow.kt create mode 100644 shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt delete mode 100644 shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetSubtitleWindowUseCaseTest.kt diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/di/Modules.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/di/Modules.kt index d25a5fd..61841a4 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/di/Modules.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/di/Modules.kt @@ -8,7 +8,7 @@ import fr.ajaury.gwenedeg.records.domain.RecordRepository import fr.ajaury.gwenedeg.records.ui.viewmodel.RecordsViewModel import fr.ajaury.gwenedeg.subtitle.data.LrcParser import fr.ajaury.gwenedeg.subtitle.data.LrcSubtitleRepository -import fr.ajaury.gwenedeg.subtitle.domain.GetSubtitleWindowUseCase +import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -20,7 +20,7 @@ val sharedModule = module { factoryOf(::InMemoryRecordRepository) { bind() } factoryOf(::LrcParser) factoryOf(::LrcSubtitleRepository) { bind() } - factoryOf(::GetSubtitleWindowUseCase) + factoryOf(::GetCurrentSubtitleIndexUseCase) viewModelOf(::RecordsViewModel) viewModelOf(::PlayerViewModel) } 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 aade3a7..3feb79a 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 @@ -1,14 +1,19 @@ package fr.ajaury.gwenedeg.player.ui +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Pause @@ -25,15 +30,17 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +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.graphics.vector.ImageVector -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -41,16 +48,17 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import fr.ajaury.gwenedeg.player.PlaybackState import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerUiState import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel -import fr.ajaury.gwenedeg.subtitle.domain.SubtitleWindow +import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine import fr.ajaury.gwenedeg.theme.GwenedegTheme import org.koin.compose.viewmodel.koinViewModel +import kotlin.math.abs @Composable fun PlayerScreen( recordTitle: String, audioResourcePath: String, subtitleResourcePath: String, - onBackClicked: () -> Unit, + onBackClicked: () -> Unit = {}, viewModel: PlayerViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -67,7 +75,8 @@ fun PlayerScreen( recordTitle = recordTitle, uiState = uiState, onMainPlayActionButtonClicked = viewModel::performMainPlayAction, - onSeek = viewModel::seekTo, + onSeekToTimePart = viewModel::seekToTimePart, + onSeekToSentence = viewModel::seekToSentence, onBackClicked = onBackClicked, ) } @@ -77,9 +86,10 @@ fun PlayerScreen( fun PlayerScreen( recordTitle: String, uiState: PlayerUiState, - onMainPlayActionButtonClicked: () -> Unit, - onSeek: (progress: Float) -> Unit, - onBackClicked: () -> Unit, + onMainPlayActionButtonClicked: () -> Unit = {}, + onSeekToTimePart: (progress: Float) -> Unit = {}, + onSeekToSentence: (position: Int) -> Unit = {}, + onBackClicked: () -> Unit = {}, ) { Scaffold( topBar = { @@ -101,69 +111,126 @@ fun PlayerScreen( ) }, ) { innerPadding -> - Box( + Column( modifier = Modifier .padding(innerPadding) .fillMaxSize() .padding(16.dp), - contentAlignment = Alignment.Center, + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - ) { - Spacer(Modifier) + SubtitleList( + lines = uiState.subtitleLines, + currentIndex = uiState.currentSubtitleIndex, + onSeek = onSeekToSentence, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) - Subtitles( - uiState = uiState, - ) + PlayerState( + uiState = uiState, + onSeek = onSeekToTimePart, + onMainPlayActionButtonClicked = onMainPlayActionButtonClicked, + ) + } + } +} - PlayerState( - uiState = uiState, - onSeek = onSeek, - onMainPlayActionButtonClicked = onMainPlayActionButtonClicked, +@Composable +private fun SubtitleList( + lines: List, + currentIndex: Int?, + onSeek: (position: Int) -> Unit, + modifier: Modifier = Modifier, +) { + val listState = rememberLazyListState() + val isDragged by listState.interactionSource.collectIsDraggedAsState() + + val centeredIndex by remember { + derivedStateOf { + val info = listState.layoutInfo + if (info.visibleItemsInfo.isEmpty()) { + -1 + } else { + val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2f + info.visibleItemsInfo.minByOrNull { abs((it.offset + it.size / 2f) - viewportCenter) }!!.index + } + } + } + + // Auto-follow: re-center the playing line when it changes, unless the user is scrolling. + LaunchedEffect(currentIndex) { + if (currentIndex != null && !listState.isScrollInProgress) { + listState.centerItem(currentIndex) + } + } + + // Snap to the centered line and seek to it once the user finishes scrolling. + LaunchedEffect(listState, lines) { + var wasDragged = false + snapshotFlow { isDragged to listState.isScrollInProgress } + .collect { (dragged, scrolling) -> + if (dragged) { + wasDragged = true + } else if (wasDragged && !scrolling) { + wasDragged = false + val targetIndex = centeredIndex + if (targetIndex in lines.indices) { + listState.centerItem(targetIndex) + onSeek(targetIndex) + } + } + } + } + + BoxWithConstraints(modifier = modifier) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + // Half-height padding so the first and last lines can reach the vertical center. + contentPadding = PaddingValues(vertical = maxHeight / 2), + ) { + itemsIndexed(lines) { index, line -> + val isCurrent = index == centeredIndex + Subtitle( + line = line, + isCurrent = isCurrent, ) } } } } -@Composable -private fun Subtitles(uiState: PlayerUiState) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Subtitle( - text = uiState.subtitles.previous, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), - ) - Subtitle( - text = uiState.subtitles.current, - style = MaterialTheme.typography.headlineSmall, - color = Color.Unspecified, - ) - Subtitle( - text = uiState.subtitles.next, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), - ) +private suspend fun LazyListState.centerItem(index: Int) { + val info = this.layoutInfo + val viewportCenter = (info.viewportStartOffset + info.viewportEndOffset) / 2 + val item = info.visibleItemsInfo.firstOrNull { it.index == index } + if (item != null) { + this.animateScrollBy((item.offset + item.size / 2 - viewportCenter).toFloat()) + } else { + this.animateScrollToItem(index) } } @Composable private fun Subtitle( - text: String, - style: TextStyle, - color: Color, + line: SubtitleLine, + isCurrent: Boolean, ) { Text( - text = text, - style = style, - color = color, + 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) + }, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), ) @@ -222,6 +289,13 @@ private fun PlaybackState.toActionContentDescription(): String = PlaybackState.PAUSED, PlaybackState.IDLE -> "Play" } +private val previewSubtitleLines = + listOf( + SubtitleLine(startMs = 0, text = "Demat deoc'h !"), + SubtitleLine(startMs = 4310, text = "Kenavo !"), + SubtitleLine(startMs = 7350, text = "Kenavo emberr !"), + ) + @Preview @Composable private fun PlayerScreenPlayingPreview() { @@ -232,14 +306,11 @@ private fun PlayerScreenPlayingPreview() { playbackState = PlaybackState.PLAYING, positionMs = 1234, durationMs = 5000, - subtitles = SubtitleWindow( - previous = "Demat deoc'h !", - current = "Kenavo !", - next = "Kenavo emberr !", - ), + subtitleLines = previewSubtitleLines, + currentSubtitleIndex = 1, ), onMainPlayActionButtonClicked = {}, - onSeek = {}, + onSeekToSentence = {}, onBackClicked = {}, ) } @@ -255,14 +326,11 @@ private fun PlayerScreenPausedPreview() { playbackState = PlaybackState.PAUSED, positionMs = 1234, durationMs = 5000, - subtitles = SubtitleWindow( - previous = "Demat deoc'h !", - current = "Kenavo !", - next = "Kenavo emberr !", - ), + subtitleLines = previewSubtitleLines, + currentSubtitleIndex = 1, ), onMainPlayActionButtonClicked = {}, - onSeek = {}, + onSeekToSentence = {}, onBackClicked = {}, ) } @@ -278,14 +346,11 @@ private fun PlayerScreenEndedPreview() { playbackState = PlaybackState.ENDED, positionMs = 1234, durationMs = 5000, - subtitles = SubtitleWindow( - previous = "Demat deoc'h !", - current = "Kenavo !", - next = "Kenavo emberr !", - ), + subtitleLines = previewSubtitleLines, + currentSubtitleIndex = 2, ), onMainPlayActionButtonClicked = {}, - onSeek = {}, + onSeekToSentence = {}, onBackClicked = {}, ) } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerUiState.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerUiState.kt index bd23c5f..3bc7a1e 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerUiState.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerUiState.kt @@ -1,13 +1,14 @@ package fr.ajaury.gwenedeg.player.ui.viewmodel import fr.ajaury.gwenedeg.player.PlaybackState -import fr.ajaury.gwenedeg.subtitle.domain.SubtitleWindow +import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine data class PlayerUiState( val playbackState: PlaybackState = PlaybackState.IDLE, val positionMs: Long = 0L, val durationMs: Long = 0L, - val subtitles: SubtitleWindow = SubtitleWindow(), + val subtitleLines: List = emptyList(), + val currentSubtitleIndex: Int? = null, ) { val progress: Float get() = if (durationMs > 0) (positionMs.toFloat() / durationMs).coerceIn(0f, 1f) else 0f 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 b7e8370..2797a14 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 @@ -6,7 +6,7 @@ import fr.ajaury.gwenedeg.core.logging.Logger import fr.ajaury.gwenedeg.player.AudioPlayer import fr.ajaury.gwenedeg.player.AudioSessionManager import fr.ajaury.gwenedeg.player.PlaybackState -import fr.ajaury.gwenedeg.subtitle.domain.GetSubtitleWindowUseCase +import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase import fr.ajaury.gwenedeg.subtitle.domain.Subtitle import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository import gwenedeg.shared.generated.resources.Res @@ -29,7 +29,7 @@ class PlayerViewModel( private val audioPlayer: AudioPlayer, private val audioSessionManager: AudioSessionManager, private val subtitleRepository: SubtitleRepository, - private val getSubtitleWindow: GetSubtitleWindowUseCase, + private val getCurrentSubtitleIndex: GetCurrentSubtitleIndexUseCase, private val logger: Logger, ) : ViewModel() { private var filePath: String? = null @@ -62,7 +62,8 @@ class PlayerViewModel( playbackState = playbackState, positionMs = currentPosition.inWholeMilliseconds, durationMs = audioPlayer.duration.inWholeMilliseconds, - subtitles = getSubtitleWindow( + subtitleLines = subtitle.lines, + currentSubtitleIndex = getCurrentSubtitleIndex( subtitle = subtitle, progressMs = currentPosition.inWholeMilliseconds, ), @@ -110,11 +111,16 @@ class PlayerViewModel( } } - fun seekTo(progress: Float) { + fun seekToTimePart(progress: Float) { val position = (progress.toDouble() * uiState.value.durationMs).milliseconds audioPlayer.seekTo(position) } + fun seekToSentence(sentenceIndex: Int) { + val matchingSubtitleLine = subtitle.value.lines.getOrNull(sentenceIndex) ?: return + audioPlayer.seekTo(matchingSubtitleLine.startMs.milliseconds) + } + fun stop() { audioPlayer.stop() } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt new file mode 100644 index 0000000..0cf3ee7 --- /dev/null +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetCurrentSubtitleIndexUseCase.kt @@ -0,0 +1,8 @@ +package fr.ajaury.gwenedeg.subtitle.domain + +class GetCurrentSubtitleIndexUseCase { + operator fun invoke( + subtitle: Subtitle, + progressMs: Long, + ): Int = subtitle.lines.indexOfLast { it.startMs <= progressMs } +} diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetSubtitleWindowUseCase.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetSubtitleWindowUseCase.kt deleted file mode 100644 index c16fc3b..0000000 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/GetSubtitleWindowUseCase.kt +++ /dev/null @@ -1,16 +0,0 @@ -package fr.ajaury.gwenedeg.subtitle.domain - -class GetSubtitleWindowUseCase { - operator fun invoke( - subtitle: Subtitle, - progressMs: Long, - ): SubtitleWindow { - val lines = subtitle.lines - val currentIndex = lines.indexOfLast { it.startMs <= progressMs } - return SubtitleWindow( - previous = lines.getOrNull(currentIndex - 1)?.text.orEmpty(), - current = lines.getOrNull(currentIndex)?.text.orEmpty(), - next = lines.getOrNull(currentIndex + 1)?.text.orEmpty(), - ) - } -} diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleWindow.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleWindow.kt deleted file mode 100644 index 36f7d9f..0000000 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/subtitle/domain/SubtitleWindow.kt +++ /dev/null @@ -1,7 +0,0 @@ -package fr.ajaury.gwenedeg.subtitle.domain - -data class SubtitleWindow( - val previous: String = "", - val current: String = "", - val next: String = "", -) diff --git a/shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt b/shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt new file mode 100644 index 0000000..04eba02 --- /dev/null +++ b/shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetCurrentSubtitleIndexUseCaseTest.kt @@ -0,0 +1,44 @@ +package fr.ajaury.gwenedeg.subtitle + +import fr.ajaury.gwenedeg.subtitle.domain.GetCurrentSubtitleIndexUseCase +import fr.ajaury.gwenedeg.subtitle.domain.Subtitle +import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine +import kotlin.test.Test +import kotlin.test.assertEquals + +class GetCurrentSubtitleIndexUseCaseTest { + private val getCurrentSubtitleIndex = GetCurrentSubtitleIndexUseCase() + + private val subtitle = + Subtitle( + lines = + listOf( + SubtitleLine(startMs = 0, text = "Demat deoc'h !"), + SubtitleLine(startMs = 4310, text = "Kenavo !"), + SubtitleLine(startMs = 7350, text = "Kenavo emberr !"), + ), + ) + + @Test + fun returns_minus_one_before_the_first_cue() { + assertEquals(-1, getCurrentSubtitleIndex(subtitle, progressMs = -1)) + } + + @Test + fun returns_the_index_at_and_after_each_cue_start() { + assertEquals(0, getCurrentSubtitleIndex(subtitle, progressMs = 0)) + assertEquals(0, getCurrentSubtitleIndex(subtitle, progressMs = 4309)) + assertEquals(1, getCurrentSubtitleIndex(subtitle, progressMs = 4310)) + assertEquals(1, getCurrentSubtitleIndex(subtitle, progressMs = 5000)) + } + + @Test + fun returns_the_last_index_past_the_end() { + assertEquals(2, getCurrentSubtitleIndex(subtitle, progressMs = 50_000)) + } + + @Test + fun returns_minus_one_for_an_empty_subtitle() { + assertEquals(-1, getCurrentSubtitleIndex(Subtitle(emptyList()), progressMs = 1_000)) + } +} diff --git a/shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetSubtitleWindowUseCaseTest.kt b/shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetSubtitleWindowUseCaseTest.kt deleted file mode 100644 index c2f9fd2..0000000 --- a/shared/src/commonTest/kotlin/fr/ajaury/gwenedeg/subtitle/GetSubtitleWindowUseCaseTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package fr.ajaury.gwenedeg.subtitle - -import fr.ajaury.gwenedeg.subtitle.domain.GetSubtitleWindowUseCase -import fr.ajaury.gwenedeg.subtitle.domain.Subtitle -import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine -import fr.ajaury.gwenedeg.subtitle.domain.SubtitleWindow -import kotlin.test.Test -import kotlin.test.assertEquals - -class GetSubtitleWindowUseCaseTest { - private val getSubtitleWindow = GetSubtitleWindowUseCase() - - private val subtitle = - Subtitle( - lines = - listOf( - SubtitleLine(startMs = 0, text = "Demat deoc'h !"), - SubtitleLine(startMs = 4310, text = "Kenavo !"), - SubtitleLine(startMs = 7350, text = "Kenavo emberr !"), - ), - ) - - @Test - fun before_the_first_cue_only_next_is_set() { - assertEquals( - SubtitleWindow(previous = "", current = "", next = "Demat deoc'h !"), - getSubtitleWindow(subtitle, progressMs = -1), - ) - } - - @Test - fun on_the_first_cue_there_is_no_previous() { - assertEquals( - SubtitleWindow(previous = "", current = "Demat deoc'h !", next = "Kenavo !"), - getSubtitleWindow(subtitle, progressMs = 0), - ) - } - - @Test - fun on_a_middle_cue_all_three_are_set() { - assertEquals( - SubtitleWindow(previous = "Demat deoc'h !", current = "Kenavo !", next = "Kenavo emberr !"), - getSubtitleWindow(subtitle, progressMs = 5000), - ) - } - - @Test - fun on_the_last_cue_there_is_no_next() { - assertEquals( - SubtitleWindow(previous = "Kenavo !", current = "Kenavo emberr !", next = ""), - getSubtitleWindow(subtitle, progressMs = 50_000), - ) - } - - @Test - fun an_empty_subtitle_yields_an_empty_window() { - assertEquals( - SubtitleWindow(), - getSubtitleWindow(Subtitle(emptyList()), progressMs = 1_000), - ) - } -}