mirror of
https://github.com/GrakovNe/lissen-android.git
synced 2026-07-31 02:18:14 -04:00
Feature/timer composable presets (#335)
This commit is contained in:
@@ -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 ->
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user