new: show previous and next subtitles
This commit is contained in:
@@ -8,7 +8,7 @@ import fr.ajaury.gwenedeg.records.domain.RecordRepository
|
|||||||
import fr.ajaury.gwenedeg.records.ui.viewmodel.RecordsViewModel
|
import fr.ajaury.gwenedeg.records.ui.viewmodel.RecordsViewModel
|
||||||
import fr.ajaury.gwenedeg.subtitle.data.LrcParser
|
import fr.ajaury.gwenedeg.subtitle.data.LrcParser
|
||||||
import fr.ajaury.gwenedeg.subtitle.data.LrcSubtitleRepository
|
import fr.ajaury.gwenedeg.subtitle.data.LrcSubtitleRepository
|
||||||
import fr.ajaury.gwenedeg.subtitle.domain.GetSubtitleTextUseCase
|
import fr.ajaury.gwenedeg.subtitle.domain.GetSubtitleWindowUseCase
|
||||||
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository
|
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository
|
||||||
import org.koin.core.module.dsl.bind
|
import org.koin.core.module.dsl.bind
|
||||||
import org.koin.core.module.dsl.factoryOf
|
import org.koin.core.module.dsl.factoryOf
|
||||||
@@ -20,7 +20,7 @@ val sharedModule = module {
|
|||||||
factoryOf(::InMemoryRecordRepository) { bind<RecordRepository>() }
|
factoryOf(::InMemoryRecordRepository) { bind<RecordRepository>() }
|
||||||
factoryOf(::LrcParser)
|
factoryOf(::LrcParser)
|
||||||
factoryOf(::LrcSubtitleRepository) { bind<SubtitleRepository>() }
|
factoryOf(::LrcSubtitleRepository) { bind<SubtitleRepository>() }
|
||||||
factoryOf(::GetSubtitleTextUseCase)
|
factoryOf(::GetSubtitleWindowUseCase)
|
||||||
viewModelOf(::RecordsViewModel)
|
viewModelOf(::RecordsViewModel)
|
||||||
viewModelOf(::PlayerViewModel)
|
viewModelOf(::PlayerViewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -39,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import fr.ajaury.gwenedeg.player.PlaybackState
|
import fr.ajaury.gwenedeg.player.PlaybackState
|
||||||
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerUiState
|
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerUiState
|
||||||
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel
|
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel
|
||||||
|
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleWindow
|
||||||
import fr.ajaury.gwenedeg.theme.GwenedegTheme
|
import fr.ajaury.gwenedeg.theme.GwenedegTheme
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@@ -81,7 +84,12 @@ fun PlayerScreen(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {},
|
title = {
|
||||||
|
Text(
|
||||||
|
text = recordTitle,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
)
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBackClicked) {
|
IconButton(onClick = onBackClicked) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -101,53 +109,105 @@ fun PlayerScreen(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Text(
|
Spacer(Modifier)
|
||||||
text = "Playing: $recordTitle",
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
Subtitles(
|
||||||
|
uiState = uiState,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
PlayerState(
|
||||||
|
uiState = uiState,
|
||||||
Text(
|
onSeek = onSeek,
|
||||||
text = uiState.subtitle,
|
onMainPlayActionButtonClicked = onMainPlayActionButtonClicked,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var seekProgress by remember { mutableStateOf<Float?>(null) }
|
|
||||||
Slider(
|
|
||||||
value = seekProgress ?: uiState.progress,
|
|
||||||
onValueChange = { seekProgress = it },
|
|
||||||
onValueChangeFinished = {
|
|
||||||
seekProgress?.let { fraction ->
|
|
||||||
if (uiState.durationMs > 0) {
|
|
||||||
onSeek(fraction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seekProgress = null
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.size(60.dp),
|
|
||||||
contentPadding = PaddingValues(0.dp),
|
|
||||||
onClick = onMainPlayActionButtonClicked,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = uiState.playbackState.toActionIcon(),
|
|
||||||
contentDescription = uiState.playbackState.toActionContentDescription(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Subtitle(
|
||||||
|
text: String,
|
||||||
|
style: TextStyle,
|
||||||
|
color: Color,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = style,
|
||||||
|
color = color,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlayerState(
|
||||||
|
uiState: PlayerUiState,
|
||||||
|
onSeek: (Float) -> Unit,
|
||||||
|
onMainPlayActionButtonClicked: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
var seekProgress by remember { mutableStateOf<Float?>(null) }
|
||||||
|
Slider(
|
||||||
|
value = seekProgress ?: uiState.progress,
|
||||||
|
onValueChange = { seekProgress = it },
|
||||||
|
onValueChangeFinished = {
|
||||||
|
seekProgress?.let { fraction ->
|
||||||
|
if (uiState.durationMs > 0) {
|
||||||
|
onSeek(fraction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seekProgress = null
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.size(60.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
onClick = onMainPlayActionButtonClicked,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = uiState.playbackState.toActionIcon(),
|
||||||
|
contentDescription = uiState.playbackState.toActionContentDescription(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun PlaybackState.toActionIcon(): ImageVector =
|
private fun PlaybackState.toActionIcon(): ImageVector =
|
||||||
when (this) {
|
when (this) {
|
||||||
PlaybackState.PLAYING -> Icons.Default.Pause
|
PlaybackState.PLAYING -> Icons.Default.Pause
|
||||||
@@ -168,7 +228,16 @@ private fun PlayerScreenPlayingPreview() {
|
|||||||
GwenedegTheme {
|
GwenedegTheme {
|
||||||
PlayerScreen(
|
PlayerScreen(
|
||||||
recordTitle = "Demat",
|
recordTitle = "Demat",
|
||||||
uiState = PlayerUiState(playbackState = PlaybackState.PLAYING, positionMs = 1234, durationMs = 5000, subtitle = "Demat deoc'h !"),
|
uiState = PlayerUiState(
|
||||||
|
playbackState = PlaybackState.PLAYING,
|
||||||
|
positionMs = 1234,
|
||||||
|
durationMs = 5000,
|
||||||
|
subtitles = SubtitleWindow(
|
||||||
|
previous = "Demat deoc'h !",
|
||||||
|
current = "Kenavo !",
|
||||||
|
next = "Kenavo emberr !",
|
||||||
|
),
|
||||||
|
),
|
||||||
onMainPlayActionButtonClicked = {},
|
onMainPlayActionButtonClicked = {},
|
||||||
onSeek = {},
|
onSeek = {},
|
||||||
onBackClicked = {},
|
onBackClicked = {},
|
||||||
@@ -182,7 +251,16 @@ private fun PlayerScreenPausedPreview() {
|
|||||||
GwenedegTheme {
|
GwenedegTheme {
|
||||||
PlayerScreen(
|
PlayerScreen(
|
||||||
recordTitle = "Demat",
|
recordTitle = "Demat",
|
||||||
uiState = PlayerUiState(playbackState = PlaybackState.PAUSED, positionMs = 1234, durationMs = 5000, subtitle = "Demat deoc'h !"),
|
uiState = PlayerUiState(
|
||||||
|
playbackState = PlaybackState.PAUSED,
|
||||||
|
positionMs = 1234,
|
||||||
|
durationMs = 5000,
|
||||||
|
subtitles = SubtitleWindow(
|
||||||
|
previous = "Demat deoc'h !",
|
||||||
|
current = "Kenavo !",
|
||||||
|
next = "Kenavo emberr !",
|
||||||
|
),
|
||||||
|
),
|
||||||
onMainPlayActionButtonClicked = {},
|
onMainPlayActionButtonClicked = {},
|
||||||
onSeek = {},
|
onSeek = {},
|
||||||
onBackClicked = {},
|
onBackClicked = {},
|
||||||
@@ -196,7 +274,16 @@ private fun PlayerScreenEndedPreview() {
|
|||||||
GwenedegTheme {
|
GwenedegTheme {
|
||||||
PlayerScreen(
|
PlayerScreen(
|
||||||
recordTitle = "Demat",
|
recordTitle = "Demat",
|
||||||
uiState = PlayerUiState(playbackState = PlaybackState.ENDED, positionMs = 1234, durationMs = 5000, subtitle = "Demat deoc'h !"),
|
uiState = PlayerUiState(
|
||||||
|
playbackState = PlaybackState.ENDED,
|
||||||
|
positionMs = 1234,
|
||||||
|
durationMs = 5000,
|
||||||
|
subtitles = SubtitleWindow(
|
||||||
|
previous = "Demat deoc'h !",
|
||||||
|
current = "Kenavo !",
|
||||||
|
next = "Kenavo emberr !",
|
||||||
|
),
|
||||||
|
),
|
||||||
onMainPlayActionButtonClicked = {},
|
onMainPlayActionButtonClicked = {},
|
||||||
onSeek = {},
|
onSeek = {},
|
||||||
onBackClicked = {},
|
onBackClicked = {},
|
||||||
|
|||||||
+2
-1
@@ -1,12 +1,13 @@
|
|||||||
package fr.ajaury.gwenedeg.player.ui.viewmodel
|
package fr.ajaury.gwenedeg.player.ui.viewmodel
|
||||||
|
|
||||||
import fr.ajaury.gwenedeg.player.PlaybackState
|
import fr.ajaury.gwenedeg.player.PlaybackState
|
||||||
|
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleWindow
|
||||||
|
|
||||||
data class PlayerUiState(
|
data class PlayerUiState(
|
||||||
val playbackState: PlaybackState = PlaybackState.IDLE,
|
val playbackState: PlaybackState = PlaybackState.IDLE,
|
||||||
val positionMs: Long = 0L,
|
val positionMs: Long = 0L,
|
||||||
val durationMs: Long = 0L,
|
val durationMs: Long = 0L,
|
||||||
val subtitle: String = "",
|
val subtitles: SubtitleWindow = SubtitleWindow(),
|
||||||
) {
|
) {
|
||||||
val progress: Float
|
val progress: Float
|
||||||
get() = if (durationMs > 0) (positionMs.toFloat() / durationMs).coerceIn(0f, 1f) else 0f
|
get() = if (durationMs > 0) (positionMs.toFloat() / durationMs).coerceIn(0f, 1f) else 0f
|
||||||
|
|||||||
+10
-4
@@ -6,7 +6,7 @@ import fr.ajaury.gwenedeg.core.logging.Logger
|
|||||||
import fr.ajaury.gwenedeg.player.AudioPlayer
|
import fr.ajaury.gwenedeg.player.AudioPlayer
|
||||||
import fr.ajaury.gwenedeg.player.AudioSessionManager
|
import fr.ajaury.gwenedeg.player.AudioSessionManager
|
||||||
import fr.ajaury.gwenedeg.player.PlaybackState
|
import fr.ajaury.gwenedeg.player.PlaybackState
|
||||||
import fr.ajaury.gwenedeg.subtitle.domain.GetSubtitleTextUseCase
|
import fr.ajaury.gwenedeg.subtitle.domain.GetSubtitleWindowUseCase
|
||||||
import fr.ajaury.gwenedeg.subtitle.domain.Subtitle
|
import fr.ajaury.gwenedeg.subtitle.domain.Subtitle
|
||||||
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository
|
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleRepository
|
||||||
import gwenedeg.shared.generated.resources.Res
|
import gwenedeg.shared.generated.resources.Res
|
||||||
@@ -29,7 +29,7 @@ class PlayerViewModel(
|
|||||||
private val audioPlayer: AudioPlayer,
|
private val audioPlayer: AudioPlayer,
|
||||||
private val audioSessionManager: AudioSessionManager,
|
private val audioSessionManager: AudioSessionManager,
|
||||||
private val subtitleRepository: SubtitleRepository,
|
private val subtitleRepository: SubtitleRepository,
|
||||||
private val getSubtitleText: GetSubtitleTextUseCase,
|
private val getSubtitleWindow: GetSubtitleWindowUseCase,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private var filePath: String? = null
|
private var filePath: String? = null
|
||||||
@@ -62,7 +62,10 @@ class PlayerViewModel(
|
|||||||
playbackState = playbackState,
|
playbackState = playbackState,
|
||||||
positionMs = currentPosition.inWholeMilliseconds,
|
positionMs = currentPosition.inWholeMilliseconds,
|
||||||
durationMs = audioPlayer.duration.inWholeMilliseconds,
|
durationMs = audioPlayer.duration.inWholeMilliseconds,
|
||||||
subtitle = getSubtitleText(subtitle, currentPosition.inWholeMilliseconds),
|
subtitles = getSubtitleWindow(
|
||||||
|
subtitle = subtitle,
|
||||||
|
progressMs = currentPosition.inWholeMilliseconds,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
@@ -90,7 +93,10 @@ class PlayerViewModel(
|
|||||||
try {
|
try {
|
||||||
subtitleRepository.getSubtitle(resourcePath)
|
subtitleRepository.getSubtitle(resourcePath)
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
logger.error(message = "Failed to load subtitle: $resourcePath", throwable = exception)
|
logger.error(
|
||||||
|
message = "Failed to load subtitle: $resourcePath",
|
||||||
|
throwable = exception,
|
||||||
|
)
|
||||||
Subtitle(emptyList())
|
Subtitle(emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
package fr.ajaury.gwenedeg.subtitle.domain
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the subtitle text to display for a given playback progress: the latest cue whose start
|
|
||||||
* is at or before [progressMs], or `null` when no cue has started yet.
|
|
||||||
*/
|
|
||||||
class GetSubtitleTextUseCase {
|
|
||||||
operator fun invoke(
|
|
||||||
subtitle: Subtitle,
|
|
||||||
progressMs: Long,
|
|
||||||
): String =
|
|
||||||
subtitle.lines
|
|
||||||
.lastOrNull { it.startMs <= progressMs }
|
|
||||||
?.text
|
|
||||||
.orEmpty()
|
|
||||||
}
|
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package fr.ajaury.gwenedeg.subtitle.domain
|
||||||
|
|
||||||
|
data class SubtitleWindow(
|
||||||
|
val previous: String = "",
|
||||||
|
val current: String = "",
|
||||||
|
val next: String = "",
|
||||||
|
)
|
||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
package fr.ajaury.gwenedeg.subtitle
|
|
||||||
|
|
||||||
import fr.ajaury.gwenedeg.subtitle.domain.GetSubtitleTextUseCase
|
|
||||||
import fr.ajaury.gwenedeg.subtitle.domain.Subtitle
|
|
||||||
import fr.ajaury.gwenedeg.subtitle.domain.SubtitleLine
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNull
|
|
||||||
|
|
||||||
class GetSubtitleTextUseCaseTest {
|
|
||||||
private val getSubtitleText = GetSubtitleTextUseCase()
|
|
||||||
|
|
||||||
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_null_before_the_first_cue() {
|
|
||||||
assertEquals("", getSubtitleText(subtitle, progressMs = -1))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun returns_the_active_cue_at_and_after_its_start() {
|
|
||||||
assertEquals("Demat deoc'h !", getSubtitleText(subtitle, progressMs = 0))
|
|
||||||
assertEquals("Demat deoc'h !", getSubtitleText(subtitle, progressMs = 4309))
|
|
||||||
assertEquals("Kenavo !", getSubtitleText(subtitle, progressMs = 4310))
|
|
||||||
assertEquals("Kenavo !", getSubtitleText(subtitle, progressMs = 5000))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun returns_the_last_cue_past_the_end() {
|
|
||||||
assertEquals("Kenavo emberr !", getSubtitleText(subtitle, progressMs = 50_000))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun returns_null_for_an_empty_subtitle() {
|
|
||||||
assertEquals("", getSubtitleText(Subtitle(emptyList()), progressMs = 1_000))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user