|
@@ -4,15 +4,17 @@ import androidx.compose.foundation.BorderStroke
|
|
|
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
|
-import androidx.compose.foundation.layout.Row
|
|
|
import androidx.compose.foundation.layout.height
|
|
|
import androidx.compose.foundation.layout.size
|
|
|
import androidx.compose.foundation.layout.width
|
|
|
import androidx.compose.foundation.lazy.LazyItemScope
|
|
|
import androidx.compose.foundation.lazy.LazyListItemInfo
|
|
|
import androidx.compose.foundation.lazy.LazyListState
|
|
|
+import androidx.compose.foundation.lazy.itemsIndexed
|
|
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
|
+import androidx.compose.foundation.text.BasicTextField
|
|
|
+import androidx.compose.foundation.text.KeyboardOptions
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
|
import androidx.compose.material3.Text
|
|
|
import androidx.compose.runtime.Composable
|
|
@@ -20,81 +22,55 @@ import androidx.compose.runtime.LaunchedEffect
|
|
|
import androidx.compose.runtime.getValue
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
import androidx.compose.runtime.remember
|
|
|
+import androidx.compose.runtime.rememberCoroutineScope
|
|
|
import androidx.compose.runtime.setValue
|
|
|
import androidx.compose.runtime.snapshotFlow
|
|
|
import androidx.compose.ui.Alignment
|
|
|
import androidx.compose.ui.Modifier
|
|
|
import androidx.compose.ui.draw.alpha
|
|
|
+import androidx.compose.ui.graphics.SolidColor
|
|
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
|
+import androidx.compose.ui.text.TextRange
|
|
|
+import androidx.compose.ui.text.TextStyle
|
|
|
+import androidx.compose.ui.text.input.ImeAction
|
|
|
+import androidx.compose.ui.text.input.KeyboardType
|
|
|
+import androidx.compose.ui.text.input.TextFieldValue
|
|
|
+import androidx.compose.ui.text.style.TextAlign
|
|
|
import androidx.compose.ui.unit.DpSize
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
import kotlinx.coroutines.flow.collectLatest
|
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
|
import kotlinx.coroutines.flow.drop
|
|
|
import kotlinx.coroutines.flow.map
|
|
|
+import kotlinx.coroutines.launch
|
|
|
import tachiyomi.presentation.core.components.material.padding
|
|
|
-import java.text.DateFormatSymbols
|
|
|
-import java.time.LocalDate
|
|
|
+import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
|
|
|
+import tachiyomi.presentation.core.util.clickableNoIndication
|
|
|
+import tachiyomi.presentation.core.util.showSoftKeyboard
|
|
|
import kotlin.math.absoluteValue
|
|
|
|
|
|
@Composable
|
|
|
-fun WheelPicker(
|
|
|
+fun WheelNumberPicker(
|
|
|
modifier: Modifier = Modifier,
|
|
|
startIndex: Int = 0,
|
|
|
- count: Int,
|
|
|
+ items: List<Number>,
|
|
|
size: DpSize = DpSize(128.dp, 128.dp),
|
|
|
onSelectionChanged: (index: Int) -> Unit = {},
|
|
|
backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
|
|
|
WheelPickerDefaults.Background(size = it)
|
|
|
},
|
|
|
- itemContent: @Composable LazyItemScope.(index: Int) -> Unit,
|
|
|
) {
|
|
|
- val lazyListState = rememberLazyListState(startIndex)
|
|
|
- val haptic = LocalHapticFeedback.current
|
|
|
-
|
|
|
- LaunchedEffect(lazyListState, onSelectionChanged) {
|
|
|
- snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
|
|
|
- .map { calculateSnappedItemIndex(lazyListState) }
|
|
|
- .distinctUntilChanged()
|
|
|
- .drop(1)
|
|
|
- .collectLatest {
|
|
|
- haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
|
|
- onSelectionChanged(it)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Box(
|
|
|
+ WheelPicker(
|
|
|
modifier = modifier,
|
|
|
- contentAlignment = Alignment.Center,
|
|
|
+ startIndex = startIndex,
|
|
|
+ items = items,
|
|
|
+ size = size,
|
|
|
+ onSelectionChanged = onSelectionChanged,
|
|
|
+ manualInputType = KeyboardType.Number,
|
|
|
+ backgroundContent = backgroundContent,
|
|
|
) {
|
|
|
- backgroundContent?.invoke(size)
|
|
|
-
|
|
|
- LazyColumn(
|
|
|
- modifier = Modifier
|
|
|
- .height(size.height)
|
|
|
- .width(size.width),
|
|
|
- state = lazyListState,
|
|
|
- contentPadding = PaddingValues(vertical = size.height / RowCount * ((RowCount - 1) / 2)),
|
|
|
- flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
|
|
|
- ) {
|
|
|
- items(count) { index ->
|
|
|
- Box(
|
|
|
- modifier = Modifier
|
|
|
- .height(size.height / RowCount)
|
|
|
- .width(size.width)
|
|
|
- .alpha(
|
|
|
- calculateAnimatedAlpha(
|
|
|
- lazyListState = lazyListState,
|
|
|
- index = index,
|
|
|
- ),
|
|
|
- ),
|
|
|
- contentAlignment = Alignment.Center,
|
|
|
- ) {
|
|
|
- itemContent(index)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ WheelPickerDefaults.Item(text = "$it")
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -102,7 +78,7 @@ fun WheelPicker(
|
|
|
fun WheelTextPicker(
|
|
|
modifier: Modifier = Modifier,
|
|
|
startIndex: Int = 0,
|
|
|
- texts: List<String>,
|
|
|
+ items: List<String>,
|
|
|
size: DpSize = DpSize(128.dp, 128.dp),
|
|
|
onSelectionChanged: (index: Int) -> Unit = {},
|
|
|
backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
|
|
@@ -112,122 +88,126 @@ fun WheelTextPicker(
|
|
|
WheelPicker(
|
|
|
modifier = modifier,
|
|
|
startIndex = startIndex,
|
|
|
- count = remember(texts) { texts.size },
|
|
|
+ items = items,
|
|
|
size = size,
|
|
|
onSelectionChanged = onSelectionChanged,
|
|
|
backgroundContent = backgroundContent,
|
|
|
) {
|
|
|
- WheelPickerDefaults.Item(text = texts[it])
|
|
|
+ WheelPickerDefaults.Item(text = it)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Composable
|
|
|
-fun WheelDatePicker(
|
|
|
+private fun <T> WheelPicker(
|
|
|
modifier: Modifier = Modifier,
|
|
|
- startDate: LocalDate = LocalDate.now(),
|
|
|
- minDate: LocalDate? = null,
|
|
|
- maxDate: LocalDate? = null,
|
|
|
- size: DpSize = DpSize(256.dp, 128.dp),
|
|
|
+ startIndex: Int = 0,
|
|
|
+ items: List<T>,
|
|
|
+ size: DpSize = DpSize(128.dp, 128.dp),
|
|
|
+ onSelectionChanged: (index: Int) -> Unit = {},
|
|
|
+ manualInputType: KeyboardType? = null,
|
|
|
backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
|
|
|
WheelPickerDefaults.Background(size = it)
|
|
|
},
|
|
|
- onSelectionChanged: (date: LocalDate) -> Unit = {},
|
|
|
+ itemContent: @Composable LazyItemScope.(item: T) -> Unit,
|
|
|
) {
|
|
|
- var internalSelection by remember { mutableStateOf(startDate) }
|
|
|
- val internalOnSelectionChange: (LocalDate) -> Unit = {
|
|
|
- internalSelection = it
|
|
|
- onSelectionChanged(internalSelection)
|
|
|
+ val haptic = LocalHapticFeedback.current
|
|
|
+ val lazyListState = rememberLazyListState(startIndex)
|
|
|
+
|
|
|
+ var internalIndex by remember { mutableStateOf(startIndex) }
|
|
|
+ val internalOnSelectionChanged: (Int) -> Unit = {
|
|
|
+ internalIndex = it
|
|
|
+ onSelectionChanged(it)
|
|
|
}
|
|
|
|
|
|
- Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
|
|
+ LaunchedEffect(lazyListState, onSelectionChanged) {
|
|
|
+ snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
|
|
|
+ .map { calculateSnappedItemIndex(lazyListState) }
|
|
|
+ .distinctUntilChanged()
|
|
|
+ .drop(1)
|
|
|
+ .collectLatest {
|
|
|
+ haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
|
|
+ internalOnSelectionChanged(it)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Box(
|
|
|
+ modifier = modifier
|
|
|
+ .height(size.height)
|
|
|
+ .width(size.width),
|
|
|
+ contentAlignment = Alignment.Center,
|
|
|
+ ) {
|
|
|
backgroundContent?.invoke(size)
|
|
|
- Row {
|
|
|
- val singularPickerSize = DpSize(
|
|
|
- width = size.width / 3,
|
|
|
- height = size.height,
|
|
|
- )
|
|
|
|
|
|
- // Day
|
|
|
- val dayOfMonths = remember(internalSelection, minDate, maxDate) {
|
|
|
- if (minDate == null && maxDate == null) {
|
|
|
- 1..internalSelection.lengthOfMonth()
|
|
|
- } else {
|
|
|
- val minDay = if (minDate?.month == internalSelection.month &&
|
|
|
- minDate?.year == internalSelection.year
|
|
|
- ) {
|
|
|
- minDate.dayOfMonth
|
|
|
- } else {
|
|
|
- 1
|
|
|
- }
|
|
|
- val maxDay = if (maxDate?.month == internalSelection.month &&
|
|
|
- maxDate?.year == internalSelection.year
|
|
|
- ) {
|
|
|
- maxDate.dayOfMonth
|
|
|
- } else {
|
|
|
- 31
|
|
|
- }
|
|
|
- minDay..maxDay.coerceAtMost(internalSelection.lengthOfMonth())
|
|
|
- }.toList()
|
|
|
+ var showManualInput by remember { mutableStateOf(false) }
|
|
|
+ if (showManualInput) {
|
|
|
+ var value by remember {
|
|
|
+ val currentString = items[internalIndex].toString()
|
|
|
+ mutableStateOf(TextFieldValue(text = currentString, selection = TextRange(currentString.length)))
|
|
|
}
|
|
|
- WheelTextPicker(
|
|
|
- size = singularPickerSize,
|
|
|
- texts = dayOfMonths.map { it.toString() },
|
|
|
- backgroundContent = null,
|
|
|
- startIndex = dayOfMonths.indexOfFirst { it == startDate.dayOfMonth }.coerceAtLeast(0),
|
|
|
- onSelectionChanged = { index ->
|
|
|
- val newDayOfMonth = dayOfMonths[index]
|
|
|
- internalOnSelectionChange(internalSelection.withDayOfMonth(newDayOfMonth))
|
|
|
- },
|
|
|
- )
|
|
|
|
|
|
- // Month
|
|
|
- val months = remember(internalSelection, minDate, maxDate) {
|
|
|
- val monthRange = if (minDate == null && maxDate == null) {
|
|
|
- 1..12
|
|
|
- } else {
|
|
|
- val minMonth = if (minDate?.year == internalSelection.year) {
|
|
|
- minDate.monthValue
|
|
|
- } else {
|
|
|
- 1
|
|
|
- }
|
|
|
- val maxMonth = if (maxDate?.year == internalSelection.year) {
|
|
|
- maxDate.monthValue
|
|
|
- } else {
|
|
|
- 12
|
|
|
+ val scope = rememberCoroutineScope()
|
|
|
+ BasicTextField(
|
|
|
+ modifier = Modifier
|
|
|
+ .align(Alignment.Center)
|
|
|
+ .showSoftKeyboard(true)
|
|
|
+ .clearFocusOnSoftKeyboardHide {
|
|
|
+ scope.launch {
|
|
|
+ items
|
|
|
+ .indexOfFirst { it.toString() == value.text }
|
|
|
+ .takeIf { it >= 0 }
|
|
|
+ ?.apply {
|
|
|
+ internalOnSelectionChanged(this)
|
|
|
+ lazyListState.scrollToItem(this)
|
|
|
+ }
|
|
|
+
|
|
|
+ showManualInput = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ value = value,
|
|
|
+ onValueChange = { value = it },
|
|
|
+ singleLine = true,
|
|
|
+ keyboardOptions = KeyboardOptions(
|
|
|
+ keyboardType = manualInputType!!,
|
|
|
+ imeAction = ImeAction.Done,
|
|
|
+ ),
|
|
|
+ textStyle = MaterialTheme.typography.titleMedium +
|
|
|
+ TextStyle(
|
|
|
+ color = MaterialTheme.colorScheme.onSurface,
|
|
|
+ textAlign = TextAlign.Center,
|
|
|
+ ),
|
|
|
+ cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ LazyColumn(
|
|
|
+ modifier = Modifier
|
|
|
+ .let {
|
|
|
+ if (manualInputType != null) {
|
|
|
+ it.clickableNoIndication { showManualInput = true }
|
|
|
+ } else {
|
|
|
+ it
|
|
|
+ }
|
|
|
+ },
|
|
|
+ state = lazyListState,
|
|
|
+ contentPadding = PaddingValues(vertical = size.height / RowCount * ((RowCount - 1) / 2)),
|
|
|
+ flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
|
|
|
+ ) {
|
|
|
+ itemsIndexed(items) { index, item ->
|
|
|
+ Box(
|
|
|
+ modifier = Modifier
|
|
|
+ .height(size.height / RowCount)
|
|
|
+ .width(size.width)
|
|
|
+ .alpha(
|
|
|
+ calculateAnimatedAlpha(
|
|
|
+ lazyListState = lazyListState,
|
|
|
+ index = index,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ contentAlignment = Alignment.Center,
|
|
|
+ ) {
|
|
|
+ itemContent(item)
|
|
|
}
|
|
|
- minMonth..maxMonth
|
|
|
}
|
|
|
- val dateFormatSymbols = DateFormatSymbols()
|
|
|
- monthRange.map { it to dateFormatSymbols.months[it - 1] }
|
|
|
}
|
|
|
- WheelTextPicker(
|
|
|
- size = singularPickerSize,
|
|
|
- texts = months.map { it.second },
|
|
|
- backgroundContent = null,
|
|
|
- startIndex = months.indexOfFirst { it.first == startDate.monthValue }.coerceAtLeast(0),
|
|
|
- onSelectionChanged = { index ->
|
|
|
- val newMonth = months[index].first
|
|
|
- internalOnSelectionChange(internalSelection.withMonth(newMonth))
|
|
|
- },
|
|
|
- )
|
|
|
-
|
|
|
- // Year
|
|
|
- val years = remember(minDate, maxDate) {
|
|
|
- val minYear = minDate?.year?.coerceAtLeast(1900) ?: 1900
|
|
|
- val maxYear = maxDate?.year?.coerceAtMost(2100) ?: 2100
|
|
|
- val yearRange = minYear..maxYear
|
|
|
- yearRange.toList()
|
|
|
- }
|
|
|
- WheelTextPicker(
|
|
|
- size = singularPickerSize,
|
|
|
- texts = years.map { it.toString() },
|
|
|
- backgroundContent = null,
|
|
|
- startIndex = years.indexOfFirst { it == startDate.year }.coerceAtLeast(0),
|
|
|
- onSelectionChanged = { index ->
|
|
|
- val newYear = years[index]
|
|
|
- internalOnSelectionChange(internalSelection.withYear(newYear))
|
|
|
- },
|
|
|
- )
|
|
|
}
|
|
|
}
|
|
|
}
|