WheelPicker: Add manual input (#9338)

pull/9290/head
Ivan Iskandar 1 year ago committed by GitHub
parent bfb7b5afd5
commit 60d8650860
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -84,6 +84,7 @@ fun AdaptiveSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = DialogProperties( properties = DialogProperties(
usePlatformDefaultWidth = false, usePlatformDefaultWidth = false,
decorFitsSystemWindows = false,
), ),
) { ) {
AdaptiveSheetImpl( AdaptiveSheetImpl(

@ -52,8 +52,8 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_UNREAD import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_UNREAD
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
import tachiyomi.presentation.core.components.WheelPicker
import tachiyomi.presentation.core.components.WheelPickerDefaults import tachiyomi.presentation.core.components.WheelPickerDefaults
import tachiyomi.presentation.core.components.WheelTextPicker
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -334,28 +334,25 @@ object SettingsLibraryScreen : SearchableSettings {
modifier = modifier, modifier = modifier,
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
WheelPickerDefaults.Background(size = DpSize(maxWidth, maxHeight)) WheelPickerDefaults.Background(size = DpSize(maxWidth, 128.dp))
val size = DpSize(width = maxWidth / 2, height = 128.dp) val size = DpSize(width = maxWidth / 2, height = 128.dp)
Row { Row {
WheelPicker( val columns = (0..10).map { getColumnValue(value = it) }
size = size, WheelTextPicker(
count = 11,
startIndex = portraitValue, startIndex = portraitValue,
items = columns,
size = size,
onSelectionChanged = onPortraitChange, onSelectionChanged = onPortraitChange,
backgroundContent = null, backgroundContent = null,
) { index -> )
WheelPickerDefaults.Item(text = getColumnValue(value = index)) WheelTextPicker(
}
WheelPicker(
size = size,
count = 11,
startIndex = landscapeValue, startIndex = landscapeValue,
items = columns,
size = size,
onSelectionChanged = onLandscapeChange, onSelectionChanged = onLandscapeChange,
backgroundContent = null, backgroundContent = null,
) { index -> )
WheelPickerDefaults.Item(text = getColumnValue(value = index))
}
} }
} }
} }

@ -30,6 +30,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.WheelNumberPicker
import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.Divider import tachiyomi.presentation.core.components.material.Divider
@ -96,10 +97,10 @@ fun TrackChapterSelector(
BaseSelector( BaseSelector(
title = stringResource(R.string.chapters), title = stringResource(R.string.chapters),
content = { content = {
WheelTextPicker( WheelNumberPicker(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
startIndex = selection, startIndex = selection,
texts = range.map { "$it" }, items = range.toList(),
onSelectionChanged = { onSelectionChange(it) }, onSelectionChanged = { onSelectionChange(it) },
) )
}, },
@ -122,7 +123,7 @@ fun TrackScoreSelector(
WheelTextPicker( WheelTextPicker(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
startIndex = selections.indexOf(selection).coerceAtLeast(0), startIndex = selections.indexOf(selection).coerceAtLeast(0),
texts = selections, items = selections,
onSelectionChanged = { onSelectionChange(selections[it]) }, onSelectionChanged = { onSelectionChange(selections[it]) },
) )
}, },

@ -4,15 +4,17 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -20,81 +22,55 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback 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.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import java.text.DateFormatSymbols import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
import java.time.LocalDate import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.showSoftKeyboard
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@Composable @Composable
fun WheelPicker( fun WheelNumberPicker(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startIndex: Int = 0, startIndex: Int = 0,
count: Int, items: List<Number>,
size: DpSize = DpSize(128.dp, 128.dp), size: DpSize = DpSize(128.dp, 128.dp),
onSelectionChanged: (index: Int) -> Unit = {}, onSelectionChanged: (index: Int) -> Unit = {},
backgroundContent: (@Composable (size: DpSize) -> Unit)? = { backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
WheelPickerDefaults.Background(size = it) WheelPickerDefaults.Background(size = it)
}, },
itemContent: @Composable LazyItemScope.(index: Int) -> Unit,
) { ) {
val lazyListState = rememberLazyListState(startIndex) WheelPicker(
val haptic = LocalHapticFeedback.current
LaunchedEffect(lazyListState, onSelectionChanged) {
snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.map { calculateSnappedItemIndex(lazyListState) }
.distinctUntilChanged()
.drop(1)
.collectLatest {
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onSelectionChanged(it)
}
}
Box(
modifier = modifier, modifier = modifier,
contentAlignment = Alignment.Center, startIndex = startIndex,
items = items,
size = size,
onSelectionChanged = onSelectionChanged,
manualInputType = KeyboardType.Number,
backgroundContent = backgroundContent,
) { ) {
backgroundContent?.invoke(size) WheelPickerDefaults.Item(text = "$it")
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)
}
}
}
} }
} }
@ -102,7 +78,7 @@ fun WheelPicker(
fun WheelTextPicker( fun WheelTextPicker(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startIndex: Int = 0, startIndex: Int = 0,
texts: List<String>, items: List<String>,
size: DpSize = DpSize(128.dp, 128.dp), size: DpSize = DpSize(128.dp, 128.dp),
onSelectionChanged: (index: Int) -> Unit = {}, onSelectionChanged: (index: Int) -> Unit = {},
backgroundContent: (@Composable (size: DpSize) -> Unit)? = { backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
@ -112,122 +88,126 @@ fun WheelTextPicker(
WheelPicker( WheelPicker(
modifier = modifier, modifier = modifier,
startIndex = startIndex, startIndex = startIndex,
count = remember(texts) { texts.size }, items = items,
size = size, size = size,
onSelectionChanged = onSelectionChanged, onSelectionChanged = onSelectionChanged,
backgroundContent = backgroundContent, backgroundContent = backgroundContent,
) { ) {
WheelPickerDefaults.Item(text = texts[it]) WheelPickerDefaults.Item(text = it)
} }
} }
@Composable @Composable
fun WheelDatePicker( private fun <T> WheelPicker(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startDate: LocalDate = LocalDate.now(), startIndex: Int = 0,
minDate: LocalDate? = null, items: List<T>,
maxDate: LocalDate? = null, size: DpSize = DpSize(128.dp, 128.dp),
size: DpSize = DpSize(256.dp, 128.dp), onSelectionChanged: (index: Int) -> Unit = {},
manualInputType: KeyboardType? = null,
backgroundContent: (@Composable (size: DpSize) -> Unit)? = { backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
WheelPickerDefaults.Background(size = it) WheelPickerDefaults.Background(size = it)
}, },
onSelectionChanged: (date: LocalDate) -> Unit = {}, itemContent: @Composable LazyItemScope.(item: T) -> Unit,
) { ) {
var internalSelection by remember { mutableStateOf(startDate) } val haptic = LocalHapticFeedback.current
val internalOnSelectionChange: (LocalDate) -> Unit = { val lazyListState = rememberLazyListState(startIndex)
internalSelection = it
onSelectionChanged(internalSelection) 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) backgroundContent?.invoke(size)
Row {
val singularPickerSize = DpSize(
width = size.width / 3,
height = size.height,
)
// Day var showManualInput by remember { mutableStateOf(false) }
val dayOfMonths = remember(internalSelection, minDate, maxDate) { if (showManualInput) {
if (minDate == null && maxDate == null) { var value by remember {
1..internalSelection.lengthOfMonth() val currentString = items[internalIndex].toString()
} else { mutableStateOf(TextFieldValue(text = currentString, selection = TextRange(currentString.length)))
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()
} }
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 scope = rememberCoroutineScope()
val months = remember(internalSelection, minDate, maxDate) { BasicTextField(
val monthRange = if (minDate == null && maxDate == null) { modifier = Modifier
1..12 .align(Alignment.Center)
} else { .showSoftKeyboard(true)
val minMonth = if (minDate?.year == internalSelection.year) { .clearFocusOnSoftKeyboardHide {
minDate.monthValue scope.launch {
} else { items
1 .indexOfFirst { it.toString() == value.text }
} .takeIf { it >= 0 }
val maxMonth = if (maxDate?.year == internalSelection.year) { ?.apply {
maxDate.monthValue internalOnSelectionChanged(this)
} else { lazyListState.scrollToItem(this)
12 }
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))
},
)
} }
} }
} }

@ -89,7 +89,9 @@ fun Modifier.showSoftKeyboard(show: Boolean): Modifier = if (show) {
* For TextField, this modifier will clear focus when soft * For TextField, this modifier will clear focus when soft
* keyboard is hidden. * keyboard is hidden.
*/ */
fun Modifier.clearFocusOnSoftKeyboardHide(): Modifier = composed { fun Modifier.clearFocusOnSoftKeyboardHide(
onFocusCleared: (() -> Unit)? = null,
): Modifier = composed {
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
var keyboardShowedSinceFocused by remember { mutableStateOf(false) } var keyboardShowedSinceFocused by remember { mutableStateOf(false) }
if (isFocused) { if (isFocused) {
@ -100,6 +102,7 @@ fun Modifier.clearFocusOnSoftKeyboardHide(): Modifier = composed {
keyboardShowedSinceFocused = true keyboardShowedSinceFocused = true
} else if (keyboardShowedSinceFocused) { } else if (keyboardShowedSinceFocused) {
focusManager.clearFocus() focusManager.clearFocus()
onFocusCleared?.invoke()
} }
} }
} }

Loading…
Cancel
Save