new: add subtitle preferences
This commit is contained in:
@@ -4,6 +4,8 @@ import fr.ajaury.gwenedeg.core.logging.di.loggingModule
|
||||
import fr.ajaury.gwenedeg.player.data.PlaybackRepositoryImpl
|
||||
import fr.ajaury.gwenedeg.player.domain.PlaybackRepository
|
||||
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel
|
||||
import fr.ajaury.gwenedeg.preferences.data.PreferencesRepositoryImpl
|
||||
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
|
||||
import fr.ajaury.gwenedeg.records.data.InMemoryRecordRepository
|
||||
import fr.ajaury.gwenedeg.records.domain.RecordRepository
|
||||
import fr.ajaury.gwenedeg.records.ui.viewmodel.RecordsViewModel
|
||||
@@ -19,6 +21,7 @@ val sharedModule = module {
|
||||
includes(loggingModule)
|
||||
factoryOf(::InMemoryRecordRepository) { bind<RecordRepository>() }
|
||||
singleOf(::PlaybackRepositoryImpl) { bind<PlaybackRepository>() }
|
||||
singleOf(::PreferencesRepositoryImpl) { bind<PreferencesRepository>() }
|
||||
viewModelOf(::RecordsViewModel)
|
||||
viewModelOf(::PlayerViewModel)
|
||||
factoryOf(::ComposeResourceReader) { bind<ResourceReader>() }
|
||||
|
||||
@@ -32,6 +32,7 @@ import fr.ajaury.gwenedeg.theme.GwenedegTheme
|
||||
|
||||
@Composable
|
||||
fun PlayerControl(
|
||||
modifier: Modifier = Modifier,
|
||||
playbackState: PlaybackState,
|
||||
playbackTiming: PlaybackTiming,
|
||||
onSeek: (Float) -> Unit = {},
|
||||
@@ -40,7 +41,7 @@ fun PlayerControl(
|
||||
onNextSentenceClicked: () -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Subtitles
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -16,6 +17,9 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -28,6 +32,7 @@ import fr.ajaury.gwenedeg.player.model.PlaybackTiming
|
||||
import fr.ajaury.gwenedeg.player.model.PlayerState
|
||||
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerUiState
|
||||
import fr.ajaury.gwenedeg.player.ui.viewmodel.PlayerViewModel
|
||||
import fr.ajaury.gwenedeg.preferences.ui.SubtitlePreferencesBottomSheet
|
||||
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
|
||||
import fr.ajaury.gwenedeg.theme.GwenedegTheme
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
@@ -57,6 +62,8 @@ fun PlayerScreen(
|
||||
onSeekToSentence = viewModel::seekToSentence,
|
||||
onPreviousSentenceClicked = viewModel::goToPreviousSentence,
|
||||
onNextSentenceClicked = viewModel::goToNextSentence,
|
||||
onShowTranscriptionChange = viewModel::setShowTranscription,
|
||||
onShowTranslationChange = viewModel::setShowTranslation,
|
||||
onBackClicked = onBackClicked,
|
||||
)
|
||||
}
|
||||
@@ -70,8 +77,12 @@ fun PlayerScreen(
|
||||
onSeekToSentence: (position: Int) -> Unit = {},
|
||||
onPreviousSentenceClicked: () -> Unit = {},
|
||||
onNextSentenceClicked: () -> Unit = {},
|
||||
onShowTranscriptionChange: (Boolean) -> Unit = {},
|
||||
onShowTranslationChange: (Boolean) -> Unit = {},
|
||||
onBackClicked: () -> Unit = {},
|
||||
) {
|
||||
var showPreferences by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -102,6 +113,14 @@ fun PlayerScreen(
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { showPreferences = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Subtitles,
|
||||
contentDescription = "Préférences de sous-titrage",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
@@ -115,6 +134,7 @@ fun PlayerScreen(
|
||||
SubtitleList(
|
||||
lines = uiState.playerState.subtitleLines,
|
||||
currentIndex = uiState.playerState.currentSubtitleIndex,
|
||||
subtitlePreferences = uiState.subtitlePreferences,
|
||||
onSeek = onSeekToSentence,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
@@ -130,6 +150,15 @@ fun PlayerScreen(
|
||||
onNextSentenceClicked = onNextSentenceClicked,
|
||||
)
|
||||
}
|
||||
|
||||
if (showPreferences) {
|
||||
SubtitlePreferencesBottomSheet(
|
||||
subtitlePreferences = uiState.subtitlePreferences,
|
||||
onShowTranscriptionChange = onShowTranscriptionChange,
|
||||
onShowTranslationChange = onShowTranslationChange,
|
||||
onDismiss = { showPreferences = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||
import fr.ajaury.gwenedeg.subtitle.model.SubtitleLine
|
||||
import fr.ajaury.gwenedeg.theme.GwenedegTheme
|
||||
import kotlin.math.abs
|
||||
@@ -38,6 +39,7 @@ fun SubtitleList(
|
||||
modifier: Modifier = Modifier,
|
||||
lines: List<SubtitleLine>,
|
||||
currentIndex: Int?,
|
||||
subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
|
||||
onSeek: (position: Int) -> Unit = {},
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
@@ -98,6 +100,7 @@ fun SubtitleList(
|
||||
Subtitle(
|
||||
line = line,
|
||||
isCurrent = isCurrent,
|
||||
subtitlePreferences = subtitlePreferences,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -121,6 +124,7 @@ private suspend fun LazyListState.centerItem(index: Int) {
|
||||
private fun Subtitle(
|
||||
line: SubtitleLine,
|
||||
isCurrent: Boolean,
|
||||
subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
|
||||
) {
|
||||
val contentColor = if (isCurrent) {
|
||||
Color.Unspecified
|
||||
@@ -133,18 +137,22 @@ private fun Subtitle(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
TranscriptionLine(
|
||||
text = line.phrase.transcription,
|
||||
isCurrent = isCurrent,
|
||||
contentColor = contentColor,
|
||||
)
|
||||
line.phrase.translation?.let { translation ->
|
||||
TranslationLine(
|
||||
text = translation,
|
||||
if (subtitlePreferences.showTranscription) {
|
||||
TranscriptionLine(
|
||||
text = line.phrase.transcription,
|
||||
isCurrent = isCurrent,
|
||||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
line.phrase.translation
|
||||
?.takeIf { subtitlePreferences.showTranslation }
|
||||
?.let { translation ->
|
||||
TranslationLine(
|
||||
text = translation,
|
||||
isCurrent = isCurrent,
|
||||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ package fr.ajaury.gwenedeg.player.ui.viewmodel
|
||||
|
||||
import fr.ajaury.gwenedeg.core.model.Phrase
|
||||
import fr.ajaury.gwenedeg.player.model.PlayerState
|
||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||
|
||||
data class PlayerUiState(
|
||||
val recordTitle: Phrase = Phrase(transcription = ""),
|
||||
val playerState: PlayerState = PlayerState(),
|
||||
val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
|
||||
)
|
||||
|
||||
+13
-1
@@ -8,6 +8,7 @@ import fr.ajaury.gwenedeg.core.model.Record
|
||||
import fr.ajaury.gwenedeg.player.domain.AudioSessionManager
|
||||
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.records.domain.RecordRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -23,6 +24,7 @@ class PlayerViewModel(
|
||||
private val recordId: Int,
|
||||
private val recordRepository: RecordRepository,
|
||||
private val playbackRepository: PlaybackRepository,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val audioSessionManager: AudioSessionManager,
|
||||
private val logger: Logger,
|
||||
) : ViewModel() {
|
||||
@@ -31,10 +33,12 @@ class PlayerViewModel(
|
||||
val uiState: StateFlow<PlayerUiState> = combine(
|
||||
record,
|
||||
playbackRepository.playerState,
|
||||
) { record, playerState ->
|
||||
preferencesRepository.subtitlePreferences,
|
||||
) { record, playerState, subtitlePreferences ->
|
||||
PlayerUiState(
|
||||
recordTitle = record?.title ?: Phrase(transcription = ""),
|
||||
playerState = playerState,
|
||||
subtitlePreferences = subtitlePreferences,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
@@ -93,6 +97,14 @@ class PlayerViewModel(
|
||||
playbackRepository.goToNextSentence()
|
||||
}
|
||||
|
||||
fun setShowTranscription(enabled: Boolean) {
|
||||
preferencesRepository.setShowTranscription(enabled)
|
||||
}
|
||||
|
||||
fun setShowTranslation(enabled: Boolean) {
|
||||
preferencesRepository.setShowTranslation(enabled)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
playbackRepository.stop()
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package fr.ajaury.gwenedeg.preferences.data
|
||||
|
||||
import fr.ajaury.gwenedeg.preferences.domain.PreferencesRepository
|
||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class PreferencesRepositoryImpl : PreferencesRepository {
|
||||
private val state = MutableStateFlow(SubtitlePreferences())
|
||||
override val subtitlePreferences: StateFlow<SubtitlePreferences> = state.asStateFlow()
|
||||
|
||||
override fun setShowTranscription(enabled: Boolean) {
|
||||
state.update { it.copy(showTranscription = enabled) }
|
||||
}
|
||||
|
||||
override fun setShowTranslation(enabled: Boolean) {
|
||||
state.update { it.copy(showTranslation = enabled) }
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package fr.ajaury.gwenedeg.preferences.domain
|
||||
|
||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface PreferencesRepository {
|
||||
val subtitlePreferences: StateFlow<SubtitlePreferences>
|
||||
|
||||
fun setShowTranscription(enabled: Boolean)
|
||||
|
||||
fun setShowTranslation(enabled: Boolean)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package fr.ajaury.gwenedeg.preferences.model
|
||||
|
||||
/**
|
||||
* User preferences controlling which subtitle lines are displayed on the player.
|
||||
*
|
||||
* @property showTranscription whether the Breton (BZH) transcription line is shown.
|
||||
* @property showTranslation whether the French (FR) translation line is shown.
|
||||
*/
|
||||
data class SubtitlePreferences(
|
||||
val showTranscription: Boolean = true,
|
||||
val showTranslation: Boolean = true,
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
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.foundation.selection.toggleable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Subtitles
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import fr.ajaury.gwenedeg.preferences.model.SubtitlePreferences
|
||||
import fr.ajaury.gwenedeg.theme.GwenedegTheme
|
||||
|
||||
|
||||
@Composable
|
||||
fun PreferenceToggle(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
icon: ImageVector? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = checked,
|
||||
role = Role.Switch,
|
||||
onValueChange = onCheckedChange,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f).padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreferenceTogglePreview() {
|
||||
GwenedegTheme {
|
||||
PreferenceToggle(
|
||||
title = "Subtitle",
|
||||
subtitle = "Subtitle",
|
||||
checked = true,
|
||||
icon = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreferenceToggleWithIconPreview() {
|
||||
GwenedegTheme {
|
||||
PreferenceToggle(
|
||||
title = "Subtitle",
|
||||
subtitle = "Subtitle",
|
||||
checked = true,
|
||||
icon = Icons.Default.Subtitles,
|
||||
)
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package fr.ajaury.gwenedeg.preferences.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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.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.theme.GwenedegTheme
|
||||
|
||||
/**
|
||||
* Bottom sheet letting the user toggle the display of the Breton transcription (BZH) and the
|
||||
* French translation (FR) on the player.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SubtitlePreferencesBottomSheet(
|
||||
subtitlePreferences: SubtitlePreferences,
|
||||
onShowTranscriptionChange: (Boolean) -> Unit = {},
|
||||
onShowTranslationChange: (Boolean) -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
SubtitlePreferencesContent(
|
||||
subtitlePreferences = subtitlePreferences,
|
||||
onShowTranscriptionChange = onShowTranscriptionChange,
|
||||
onShowTranslationChange = onShowTranslationChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubtitlePreferencesContent(
|
||||
subtitlePreferences: SubtitlePreferences,
|
||||
onShowTranscriptionChange: (Boolean) -> Unit = {},
|
||||
onShowTranslationChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Sous-titres",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
PreferenceToggle(
|
||||
title = "Transcription (BZH)",
|
||||
subtitle = "Afficher la transcription bretonne",
|
||||
checked = subtitlePreferences.showTranscription,
|
||||
onCheckedChange = onShowTranscriptionChange,
|
||||
)
|
||||
PreferenceToggle(
|
||||
title = "Traduction (FR)",
|
||||
subtitle = "Afficher la traduction française",
|
||||
checked = subtitlePreferences.showTranslation,
|
||||
onCheckedChange = onShowTranslationChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SubtitlePreferencesContentPreview() {
|
||||
GwenedegTheme {
|
||||
SubtitlePreferencesContent(
|
||||
subtitlePreferences = SubtitlePreferences(
|
||||
showTranscription = true,
|
||||
showTranslation = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user