Jelajahi Sumber

WheelPicker: Add manual input (#9338)

Ivan Iskandar 1 tahun lalu
induk
melakukan
60d8650860

+ 1 - 0
app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt

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

+ 11 - 14
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt

@@ -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_NON_COMPLETED
 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.WheelTextPicker
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -334,28 +334,25 @@ object SettingsLibraryScreen : SearchableSettings {
             modifier = modifier,
             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)
             Row {
-                WheelPicker(
-                    size = size,
-                    count = 11,
+                val columns = (0..10).map { getColumnValue(value = it) }
+                WheelTextPicker(
                     startIndex = portraitValue,
+                    items = columns,
+                    size = size,
                     onSelectionChanged = onPortraitChange,
                     backgroundContent = null,
-                ) { index ->
-                    WheelPickerDefaults.Item(text = getColumnValue(value = index))
-                }
-                WheelPicker(
-                    size = size,
-                    count = 11,
+                )
+                WheelTextPicker(
                     startIndex = landscapeValue,
+                    items = columns,
+                    size = size,
                     onSelectionChanged = onLandscapeChange,
                     backgroundContent = null,
-                ) { index ->
-                    WheelPickerDefaults.Item(text = getColumnValue(value = index))
-                }
+                )
             }
         }
     }

+ 4 - 3
app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt

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

+ 124 - 144
presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt

@@ -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))
-                },
-            )
         }
     }
 }

+ 4 - 1
presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt

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