Feature/timer composable presets (#335)

This commit is contained in:
Max Grakov
2026-01-14 13:25:23 +03:00
committed by GitHub
parent 5c13f32bf0
commit 87f8a71fc0
3 changed files with 104 additions and 10 deletions

View File

@@ -37,9 +37,10 @@ fun SleepTimerSlider(
onUpdate: (TimerOption?) -> Unit,
) {
val sliderRange = INTERNAL_MIN_VALUE..INTERNAL_MAX_VALUE
val floatRange = sliderRange.first.toFloat()..sliderRange.last.toFloat()
val onValueUpdate: (Float) -> Unit = { value ->
onUpdate(value.toTimerOption())
onUpdate(value.coerceIn(floatRange).toTimerOption())
}
val sliderState =
@@ -56,15 +57,17 @@ fun SleepTimerSlider(
}
LaunchedEffect(option) {
sliderState.animateDecayTo(option.toInternalValue().toFloat())
sliderState.animateDecayTo(option.toInternalValue().toFloat().coerceIn(floatRange))
}
val clampedCurrent = sliderState.current.coerceIn(floatRange)
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = sliderState.current.toLabelText(libraryType, context),
text = clampedCurrent.toLabelText(libraryType, context),
style = typography.headlineSmall,
)
@@ -85,12 +88,12 @@ fun SleepTimerSlider(
val visibleSegmentCount = (visibleSegments + 1) / 2
val minIndex =
(sliderState.current - visibleSegmentCount)
(clampedCurrent - visibleSegmentCount)
.roundToInt()
.coerceAtLeast(sliderRange.first)
val maxIndex =
(sliderState.current + visibleSegmentCount)
(clampedCurrent + visibleSegmentCount)
.roundToInt()
.coerceAtMost(sliderRange.last)
@@ -99,7 +102,7 @@ fun SleepTimerSlider(
for (index in minIndex..maxIndex) {
SpeedSliderSegment(
index = index,
currentValue = sliderState.current,
currentValue = clampedCurrent,
segmentWidth = segmentWidth,
segmentPixelWidth = segmentPixelWidth,
centerPixel = centerPixel,
@@ -120,7 +123,8 @@ private fun TimerOption?.toInternalValue(): Int =
}
private fun Float.toTimerOption(): TimerOption? {
val value = roundToInt()
val value = roundToInt().coerceIn(INTERNAL_MIN_VALUE, INTERNAL_MAX_VALUE)
return when (value) {
INTERNAL_DISABLED -> null
INTERNAL_CHAPTER_END -> CurrentEpisodeTimerOption
@@ -132,7 +136,8 @@ private fun Float.toLabelText(
libraryType: LibraryType,
context: Context,
): String {
val value = roundToInt()
val value = roundToInt().coerceIn(INTERNAL_MIN_VALUE, INTERNAL_MAX_VALUE)
return when (value) {
INTERNAL_DISABLED -> context.getString(R.string.timer_option_disabled)
INTERNAL_CHAPTER_END ->

View File

@@ -48,11 +48,11 @@ class SliderState(
}
companion object {
private const val maxSpeed = 25f
private const val maxSpeed = 10f
private val springSpec =
FloatSpringSpec(
dampingRatio = Spring.DampingRatioMediumBouncy,
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)

View File

@@ -1,20 +1,41 @@
package org.grakovne.lissen.ui.screens.player.composable
import android.view.View
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.grakovne.lissen.R
import org.grakovne.lissen.common.withHaptic
import org.grakovne.lissen.lib.domain.DurationTimerOption
import org.grakovne.lissen.lib.domain.LibraryType
import org.grakovne.lissen.lib.domain.TimerOption
import org.grakovne.lissen.ui.components.slider.SleepTimerSlider
@@ -27,7 +48,9 @@ fun TimerComposable(
onOptionSelected: (TimerOption?) -> Unit,
onDismissRequest: () -> Unit,
) {
val view = LocalView.current
val context = LocalContext.current
var selectedOption by remember { mutableStateOf(currentOption) }
ModalBottomSheet(
containerColor = colorScheme.background,
@@ -56,7 +79,73 @@ fun TimerComposable(
.padding(vertical = 16.dp),
onUpdate = { onOptionSelected(it) },
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
OptionPresets.forEach { value ->
FilledTonalButton(
onClick = {
withHaptic(view) {
selectedOption = value
onOptionSelected(value)
}
},
modifier = Modifier.size(56.dp),
shape = CircleShape,
colors =
ButtonDefaults.filledTonalButtonColors(
containerColor =
if (selectedOption == value) {
colorScheme.primary
} else {
colorScheme.surfaceContainer
},
contentColor =
if (selectedOption == value) {
colorScheme.onPrimary
} else {
colorScheme.onSurfaceVariant
},
),
contentPadding = PaddingValues(0.dp),
) {
if (value == null) {
val fontSize = typography.labelMedium.fontSize
val iconSize = with(LocalDensity.current) { fontSize.toDp() } * 1.5f
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = null,
modifier = Modifier.size(iconSize),
)
} else {
Text(
text = value.duration.toString(),
style =
if (selectedOption == value) {
typography.labelMedium.copy(fontWeight = FontWeight.Bold)
} else {
typography.labelMedium
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
}
},
)
}
private val OptionPresets =
listOf(
null,
DurationTimerOption(10),
DurationTimerOption(15),
DurationTimerOption(30),
DurationTimerOption(60),
)