diff --git a/data/playback/src/commonMain/kotlin/bzh/ajaury/chombev/player/data/PlaybackRepositoryImpl.kt b/data/playback/src/commonMain/kotlin/bzh/ajaury/chombev/player/data/PlaybackRepositoryImpl.kt
index 4666592..9e40849 100644
--- a/data/playback/src/commonMain/kotlin/bzh/ajaury/chombev/player/data/PlaybackRepositoryImpl.kt
+++ b/data/playback/src/commonMain/kotlin/bzh/ajaury/chombev/player/data/PlaybackRepositoryImpl.kt
@@ -186,11 +186,16 @@ internal class PlaybackRepositoryImpl(
}
/**
- * Inserts the configured silent break between spoken phrases: when continuous playback reaches
- * the next phrase, playback pauses just before that phrase starts, then resumes after the break.
+ * Reacts to reaching the next spoken phrase during continuous playback, just before that phrase
+ * starts. Depending on preferences it either:
+ * - pauses and waits for the user to resume, when auto-pause is enabled, or
+ * - pauses for the configured silent break and then resumes on its own.
*
- * Breaks only happen on automatic advancement — manual seeking and previous/next navigation are
- * ignored via [suppressBreaks].
+ * Auto-pause takes precedence: while it is on, the break between phrases is ignored.
+ *
+ * This only happens on automatic advancement — manual seeking and previous/next navigation are
+ * ignored because they cancel the running break job and never re-enter [PlaybackState.PLAYING]
+ * through this path.
*/
private suspend fun observePhraseBreaks() {
phraseBreakJob?.cancel()
@@ -203,6 +208,11 @@ internal class PlaybackRepositoryImpl(
if (playbackState.firstOrNull() != PlaybackState.PLAYING) return@onEach
val playbackPreferences = preferencesRepository.playbackPreferences.first()
+ if (playbackPreferences.autoPause) {
+ audioPlayer.pause()
+ return@onEach
+ }
+
val breakDuration = playbackPreferences.breakBetweenPhrases
if (breakDuration <= Duration.ZERO) return@onEach
diff --git a/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/data/InMemoryPreferencesRepository.kt b/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/data/InMemoryPreferencesRepository.kt
index 60c3af3..6064e3d 100644
--- a/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/data/InMemoryPreferencesRepository.kt
+++ b/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/data/InMemoryPreferencesRepository.kt
@@ -43,4 +43,8 @@ internal class InMemoryPreferencesRepository : PreferencesRepository {
override suspend fun setBreakBetweenPhrases(duration: Duration) {
playbackState.update { it.copy(breakBetweenPhrases = duration) }
}
+
+ override suspend fun setAutoPause(enabled: Boolean) {
+ playbackState.update { it.copy(autoPause = enabled) }
+ }
}
diff --git a/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/domain/PreferencesRepository.kt b/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/domain/PreferencesRepository.kt
index a996b06..439bdc5 100644
--- a/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/domain/PreferencesRepository.kt
+++ b/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/domain/PreferencesRepository.kt
@@ -21,4 +21,6 @@ interface PreferencesRepository {
suspend fun setPlaybackSpeed(speed: Float)
suspend fun setBreakBetweenPhrases(duration: Duration)
+
+ suspend fun setAutoPause(enabled: Boolean)
}
diff --git a/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/model/PlaybackPreferences.kt b/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/model/PlaybackPreferences.kt
index 2c2661d..cdbfdbb 100644
--- a/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/model/PlaybackPreferences.kt
+++ b/data/preferences/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/model/PlaybackPreferences.kt
@@ -10,10 +10,13 @@ import kotlin.time.Duration.Companion.seconds
* [SPEED_STEP] increments. [DEFAULT_SPEED] is the normal, unaltered speed.
* @property breakBetweenPhrases silent pause inserted when moving from one phrase to the next,
* between [DEFAULT_BREAK_BETWEEN_PHRASES] and [MAX_BREAK_BETWEEN_PHRASES] in [BREAK_STEP] steps.
+ * @property autoPause when enabled, playback stops at the end of every phrase and waits for the
+ * user to resume. This takes precedence over [breakBetweenPhrases], which is then ignored.
*/
data class PlaybackPreferences(
val speed: Float = DEFAULT_SPEED,
val breakBetweenPhrases: Duration = DEFAULT_BREAK_BETWEEN_PHRASES,
+ val autoPause: Boolean = DEFAULT_AUTO_PAUSE,
) {
companion object {
const val DEFAULT_SPEED = 1f
@@ -24,5 +27,7 @@ data class PlaybackPreferences(
val DEFAULT_BREAK_BETWEEN_PHRASES: Duration = Duration.ZERO
val MAX_BREAK_BETWEEN_PHRASES: Duration = 5.seconds
val BREAK_STEP: Duration = 1.seconds
+
+ const val DEFAULT_AUTO_PAUSE = false
}
}
diff --git a/data/preferences/src/dataStoreMain/kotlin/bzh/ajaury/chombev/preferences/data/DataStorePreferencesRepository.kt b/data/preferences/src/dataStoreMain/kotlin/bzh/ajaury/chombev/preferences/data/DataStorePreferencesRepository.kt
index b415f52..37a8084 100644
--- a/data/preferences/src/dataStoreMain/kotlin/bzh/ajaury/chombev/preferences/data/DataStorePreferencesRepository.kt
+++ b/data/preferences/src/dataStoreMain/kotlin/bzh/ajaury/chombev/preferences/data/DataStorePreferencesRepository.kt
@@ -44,6 +44,8 @@ internal class DataStorePreferencesRepository(
override suspend fun setBreakBetweenPhrases(duration: Duration) =
edit { it[BreakBetweenPhrasesSecondsKey] = duration.inWholeSeconds.toInt() }
+ override suspend fun setAutoPause(enabled: Boolean) = edit { it[AutoPauseKey] = enabled }
+
private suspend fun edit(transform: (MutablePreferences) -> Unit) {
dataStore.edit(transform)
}
@@ -63,6 +65,7 @@ internal class DataStorePreferencesRepository(
speed = this[PlaybackSpeedKey] ?: PlaybackPreferences.DEFAULT_SPEED,
breakBetweenPhrases = this[BreakBetweenPhrasesSecondsKey]?.seconds
?: PlaybackPreferences.DEFAULT_BREAK_BETWEEN_PHRASES,
+ autoPause = this[AutoPauseKey] ?: PlaybackPreferences.DEFAULT_AUTO_PAUSE,
)
private companion object {
@@ -72,5 +75,6 @@ internal class DataStorePreferencesRepository(
val ShowPhoneticMarkersKey = booleanPreferencesKey("show_phonetic_marker")
val PlaybackSpeedKey = floatPreferencesKey("playback_speed")
val BreakBetweenPhrasesSecondsKey = intPreferencesKey("break_between_phrases_seconds")
+ val AutoPauseKey = booleanPreferencesKey("auto_pause")
}
}
diff --git a/data/resources/src/commonMain/composeResources/values-br/strings.xml b/data/resources/src/commonMain/composeResources/values-br/strings.xml
index dabe1cf..b9000f3 100644
--- a/data/resources/src/commonMain/composeResources/values-br/strings.xml
+++ b/data/resources/src/commonMain/composeResources/values-br/strings.xml
@@ -25,6 +25,8 @@
Arventennoù al lenn
Ehan etre ar frazennoù
%1$d s
+ Ehan emgefreek
+ Ehanañ goude pep frazenn
Distreiñ
Frazenn a-raok
diff --git a/data/resources/src/commonMain/composeResources/values/strings.xml b/data/resources/src/commonMain/composeResources/values/strings.xml
index 0f49516..24e15c9 100644
--- a/data/resources/src/commonMain/composeResources/values/strings.xml
+++ b/data/resources/src/commonMain/composeResources/values/strings.xml
@@ -25,6 +25,8 @@
Réglages de lecture
Pause entre les phrases
%1$d s
+ Pause automatique
+ Mettre en pause après chaque phrase
Retour
Phrase précédente
diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt
index 617ca5c..91078e8 100644
--- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt
+++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlaybackSpeedBottomSheet.kt
@@ -15,7 +15,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import bzh.ajaury.chombev.player.ui.components.LabeledSlider
import bzh.ajaury.chombev.preferences.model.PlaybackPreferences
+import bzh.ajaury.chombev.preferences.ui.PreferenceToggle
import bzh.ajaury.chombev.resources.generated.resources.Res
+import bzh.ajaury.chombev.resources.generated.resources.auto_pause_description
+import bzh.ajaury.chombev.resources.generated.resources.auto_pause_title
import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_title
import bzh.ajaury.chombev.resources.generated.resources.break_between_phrases_value
import bzh.ajaury.chombev.resources.generated.resources.playback_speed_title
@@ -23,16 +26,19 @@ import org.jetbrains.compose.resources.stringResource
import kotlin.math.roundToInt
/**
- * Bottom sheet letting the user adjust playback: the audio [speed] and the [breakSeconds] silent
- * pause inserted between phrases.
+ * Bottom sheet letting the user adjust playback: the audio [speed], whether playback stops after
+ * each phrase ([autoPause]), and the [breakSeconds] silent pause inserted between phrases. When
+ * [autoPause] is on, the break setting is ignored and shown as disabled.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlaybackSpeedBottomSheet(
speed: Float,
breakSeconds: Float,
+ autoPause: Boolean,
onSpeedChange: (Float) -> Unit = {},
onBreakChange: (Float) -> Unit = {},
+ onAutoPauseChange: (Boolean) -> Unit = {},
onDismiss: () -> Unit = {},
sheetState: SheetState = rememberModalBottomSheetState(),
) {
@@ -43,8 +49,10 @@ fun PlaybackSpeedBottomSheet(
PlaybackSpeedContent(
speed = speed,
breakSeconds = breakSeconds,
+ autoPause = autoPause,
onSpeedChange = onSpeedChange,
onBreakChange = onBreakChange,
+ onAutoPauseChange = onAutoPauseChange,
)
}
}
@@ -53,8 +61,10 @@ fun PlaybackSpeedBottomSheet(
private fun PlaybackSpeedContent(
speed: Float,
breakSeconds: Float,
+ autoPause: Boolean,
onSpeedChange: (Float) -> Unit = {},
onBreakChange: (Float) -> Unit = {},
+ onAutoPauseChange: (Boolean) -> Unit = {},
) {
// Number of discrete stops strictly between the endpoints (e.g. 0.5..1.0 by 0.1 → 4).
val speedSteps = with(PlaybackPreferences) {
@@ -79,6 +89,12 @@ private fun PlaybackSpeedContent(
steps = speedSteps,
onValueChange = onSpeedChange,
)
+ PreferenceToggle(
+ title = stringResource(Res.string.auto_pause_title),
+ subtitle = stringResource(Res.string.auto_pause_description),
+ checked = autoPause,
+ onCheckedChange = onAutoPauseChange,
+ )
LabeledSlider(
title = stringResource(Res.string.break_between_phrases_title),
valueLabel = stringResource(
@@ -89,6 +105,8 @@ private fun PlaybackSpeedContent(
valueRange = 0f..maxBreakSeconds.toFloat(),
steps = breakSteps,
onValueChange = onBreakChange,
+ // Auto-pause supersedes the break between phrases, so disable it while on.
+ enabled = !autoPause,
)
}
}
@@ -106,6 +124,19 @@ private fun PlaybackSpeedContentPreview() {
PlaybackSpeedContent(
speed = 0.7f,
breakSeconds = 2f,
+ autoPause = false,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun PlaybackSpeedContentAutoPausePreview() {
+ MaterialTheme {
+ PlaybackSpeedContent(
+ speed = 0.7f,
+ breakSeconds = 2f,
+ autoPause = true,
)
}
}
diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt
index f2f70f6..d3b4a40 100644
--- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt
+++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/PlayerScreen.kt
@@ -82,6 +82,7 @@ fun PlayerScreen(
onTextSizeIncrease = viewModel::increaseSubtitleTextSize,
onSpeedChange = viewModel::setPlaybackSpeed,
onBreakChange = viewModel::setBreakBetweenPhrases,
+ onAutoPauseChange = viewModel::setAutoPause,
onBackClicked = onBackClicked,
)
}
@@ -102,6 +103,7 @@ fun PlayerScreen(
onTextSizeIncrease: () -> Unit = {},
onSpeedChange: (Float) -> Unit = {},
onBreakChange: (Float) -> Unit = {},
+ onAutoPauseChange: (Boolean) -> Unit = {},
onBackClicked: () -> Unit = {},
) {
var showPreferences by remember { mutableStateOf(false) }
@@ -166,8 +168,10 @@ fun PlayerScreen(
PlaybackSpeedBottomSheet(
speed = uiState.playbackSpeed,
breakSeconds = uiState.breakBetweenPhrases.inWholeSeconds.toFloat(),
+ autoPause = uiState.autoPause,
onSpeedChange = onSpeedChange,
onBreakChange = onBreakChange,
+ onAutoPauseChange = onAutoPauseChange,
onDismiss = { showSpeed = false },
)
}
diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/components/LabeledSlider.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/components/LabeledSlider.kt
index a064ba2..0a8bffd 100644
--- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/components/LabeledSlider.kt
+++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/components/LabeledSlider.kt
@@ -21,7 +21,11 @@ fun LabeledSlider(
valueRange: ClosedFloatingPointRange,
steps: Int,
onValueChange: (Float) -> Unit = {},
+ enabled: Boolean = true,
) {
+ // Dim the labels to match the Slider's disabled appearance.
+ val contentAlpha = if (enabled) 1f else 0.38f
+
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -31,11 +35,12 @@ fun LabeledSlider(
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = contentAlpha),
)
Text(
text = valueLabel,
style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.primary,
+ color = MaterialTheme.colorScheme.primary.copy(alpha = contentAlpha),
)
}
Slider(
@@ -43,6 +48,7 @@ fun LabeledSlider(
onValueChange = onValueChange,
valueRange = valueRange,
steps = steps,
+ enabled = enabled,
)
}
}
@@ -60,3 +66,18 @@ private fun LabeledSliderPreview() {
)
}
}
+
+@Preview
+@Composable
+private fun LabeledSliderDisabledPreview() {
+ MaterialTheme {
+ LabeledSlider(
+ title = "Playback speed",
+ valueLabel = "x0.7",
+ value = 0.7f,
+ valueRange = 0.5f..2f,
+ steps = 10,
+ enabled = false,
+ )
+ }
+}
diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerUiState.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerUiState.kt
index 6b122f1..c411806 100644
--- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerUiState.kt
+++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerUiState.kt
@@ -14,6 +14,7 @@ data class PlayerUiState(
val subtitlePreferences: SubtitlePreferences = SubtitlePreferences(),
val playbackSpeed: Float = PlaybackPreferences.DEFAULT_SPEED,
val breakBetweenPhrases: Duration = PlaybackPreferences.DEFAULT_BREAK_BETWEEN_PHRASES,
+ val autoPause: Boolean = PlaybackPreferences.DEFAULT_AUTO_PAUSE,
) {
val canGoToPreviousSentence: Boolean
get() = playerState.playbackTiming.position > 2.seconds
@@ -21,8 +22,8 @@ data class PlayerUiState(
val canGoToNextSentence: Boolean
get() = (playerState.currentSubtitleIndex ?: Int.MAX_VALUE) < playerState.subtitleLines.lastIndex
- /** True when either playback setting differs from its default, used to highlight the button. */
+ /** True when any playback setting differs from its default, used to highlight the button. */
val isPlaybackModified: Boolean
get() = playbackSpeed != PlaybackPreferences.DEFAULT_SPEED ||
- breakBetweenPhrases != PlaybackPreferences.DEFAULT_BREAK_BETWEEN_PHRASES
+ breakBetweenPhrases != PlaybackPreferences.DEFAULT_BREAK_BETWEEN_PHRASES || autoPause
}
diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerViewModel.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerViewModel.kt
index 59765d7..25b4a2d 100644
--- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerViewModel.kt
+++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/player/ui/viewmodel/PlayerViewModel.kt
@@ -46,6 +46,7 @@ class PlayerViewModel(
subtitlePreferences = subtitlePreferences,
playbackSpeed = playbackPreferences.speed,
breakBetweenPhrases = playbackPreferences.breakBetweenPhrases,
+ autoPause = playbackPreferences.autoPause,
)
}.stateIn(
scope = viewModelScope,
@@ -136,6 +137,12 @@ class PlayerViewModel(
}
}
+ fun setAutoPause(enabled: Boolean) {
+ viewModelScope.launch {
+ preferencesRepository.setAutoPause(enabled)
+ }
+ }
+
fun setShowTranscription(enabled: Boolean) {
viewModelScope.launch {
preferencesRepository.setShowTranscription(enabled)
diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/PreferenceToggle.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/PreferenceToggle.kt
index 8a891c1..68219dc 100644
--- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/PreferenceToggle.kt
+++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/PreferenceToggle.kt
@@ -46,7 +46,7 @@ fun PreferenceToggle(
Column(modifier = Modifier.weight(1f).padding(vertical = 8.dp)) {
Text(
text = title,
- style = MaterialTheme.typography.bodyLarge,
+ style = MaterialTheme.typography.titleMedium,
)
Text(
text = subtitle,
diff --git a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt
index dc4909a..95283ed 100644
--- a/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt
+++ b/feature/records/src/commonMain/kotlin/bzh/ajaury/chombev/preferences/ui/SubtitlePreferencesBottomSheet.kt
@@ -85,7 +85,7 @@ private fun SubtitlePreferencesContent(
) {
Text(
text = stringResource(Res.string.subtitle_preferences_title),
- style = MaterialTheme.typography.titleMedium,
+ style = MaterialTheme.typography.titleLarge,
)
PreferenceToggle(
title = stringResource(Res.string.subtitle_transcription_label),