11package com.philkes.notallyx.presentation.activity.main.fragment.settings
22
3+ import android.annotation.SuppressLint
34import android.content.Context
45import android.hardware.biometrics.BiometricManager
56import android.net.Uri
67import android.os.Build
8+ import android.os.Handler
9+ import android.os.Looper
710import android.text.method.PasswordTransformationMethod
811import android.view.LayoutInflater
12+ import android.view.MotionEvent
913import android.view.View
14+ import android.view.inputmethod.EditorInfo
1015import androidx.core.view.isVisible
16+ import androidx.core.widget.doAfterTextChanged
1117import androidx.documentfile.provider.DocumentFile
1218import com.google.android.material.color.DynamicColors
1319import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -23,11 +29,14 @@ import com.philkes.notallyx.databinding.DialogSelectionBoxBinding
2329import com.philkes.notallyx.databinding.DialogTextInputBinding
2430import com.philkes.notallyx.databinding.PreferenceBinding
2531import com.philkes.notallyx.databinding.PreferenceSeekbarBinding
32+ import com.philkes.notallyx.databinding.PreferenceStepperBinding
2633import com.philkes.notallyx.presentation.checkedTag
34+ import com.philkes.notallyx.presentation.hideKeyboard
2735import com.philkes.notallyx.presentation.select
2836import com.philkes.notallyx.presentation.setCancelButton
2937import com.philkes.notallyx.presentation.setTextSizeSp
3038import com.philkes.notallyx.presentation.showAndFocus
39+ import com.philkes.notallyx.presentation.showKeyboard
3140import com.philkes.notallyx.presentation.showToast
3241import com.philkes.notallyx.presentation.view.misc.MenuDialog
3342import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel
@@ -501,43 +510,157 @@ fun PreferenceSeekbarBinding.setupTextSizePreference(
501510 }
502511}
503512
504- fun PreferenceSeekbarBinding.setupAutoSaveIdleTime (
505- preference : IntPreference ,
513+ @SuppressLint(" ClickableViewAccessibility" )
514+ fun PreferenceStepperBinding.setup (
515+ value : Int ,
516+ titleResId : Int ,
517+ min : Int ,
518+ max : Int ,
506519 context : Context ,
507- value : Int = preference.value,
520+ enabled : Boolean = true,
521+ labelFormatter : ((Int ) -> String )? = null,
508522 onChange : (newValue: Int ) -> Unit ,
509523) {
510- Slider .apply {
511- setLabelFormatter { sliderValue ->
512- if (sliderValue == - 1f ) {
513- context.getString(R .string.disabled)
514- } else " ${sliderValue.toInt()} s"
524+ val initialValue = value.coerceIn(min, max)
525+ Title .setText(titleResId)
526+ ValueInput .setText(labelFormatter?.invoke(initialValue) ? : initialValue.toString())
527+ ValueInput .isEnabled = enabled
528+
529+ fun updateButtons (value : Int ) {
530+ MinusButton .isEnabled = enabled && value > min
531+ PlusButton .isEnabled = enabled && value < max
532+ }
533+
534+ updateButtons(initialValue)
535+
536+ val handler = Handler (Looper .getMainLooper())
537+ fun updateValue (increment : Int , commit : Boolean ): Boolean {
538+ val newValue = (ValueInput .tag as ? Int ? : initialValue) + increment
539+ val valid = newValue in min.. max
540+ if (valid) {
541+ ValueInput .tag = newValue
542+ ValueInput .setText(labelFormatter?.invoke(newValue) ? : newValue.toString())
543+ updateButtons(newValue)
544+ if (commit) {
545+ onChange(newValue)
546+ }
547+ }
548+ ValueInput .clearFocus()
549+ return valid
550+ }
551+
552+ fun stepperRunnable (isIncrement : Boolean ) =
553+ object : Runnable {
554+ override fun run () {
555+ if (updateValue(if (isIncrement) 1 else - 1 , false )) {
556+ handler.postDelayed(this , 100 )
557+ }
558+ }
515559 }
516- addOnChangeListener { _, value, _ ->
517- if (value == - 1f ) {
518- setAlpha(0.6f ) // Reduce opacity to make it look disabled
560+ var runnable: Runnable ? = null
561+ val startAutoIncrement = { isIncrement: Boolean ->
562+ runnable?.let { handler.removeCallbacks(it) }
563+ runnable =
564+ if (isIncrement) stepperRunnable(isIncrement = true )
565+ else stepperRunnable(isIncrement = false )
566+ handler.postDelayed(runnable!! , 100 )
567+ }
568+
569+ MinusButton .apply {
570+ setOnClickListener { updateValue(- 1 , true ) }
571+ setOnLongClickListener {
572+ startAutoIncrement(false )
573+ true
574+ }
575+ setOnTouchListener { _, event ->
576+ if (
577+ runnable != null &&
578+ (event.action == MotionEvent .ACTION_UP ||
579+ event.action == MotionEvent .ACTION_CANCEL )
580+ ) {
581+ handler.removeCallbacks(runnable!! )
582+ onChange(ValueInput .tag as ? Int ? : value)
583+ }
584+ false
585+ }
586+ }
587+
588+ PlusButton .apply {
589+ setOnClickListener { updateValue(1 , true ) }
590+ setOnLongClickListener {
591+ startAutoIncrement(true )
592+ true
593+ }
594+ setOnTouchListener { _, event ->
595+ if (
596+ runnable != null &&
597+ (event.action == MotionEvent .ACTION_UP ||
598+ event.action == MotionEvent .ACTION_CANCEL )
599+ ) {
600+ handler.removeCallbacks(runnable!! )
601+ onChange(ValueInput .tag as ? Int ? : value)
602+ }
603+ false
604+ }
605+ }
606+
607+ ValueInput .apply {
608+ tag = value
609+ doAfterTextChanged { text ->
610+ if (ValueInput .hasFocus()) {
611+ val newValue = text.toString().toIntOrNull()
612+ if (newValue != null ) {
613+ val clampedValue = newValue.coerceIn(min, max)
614+ if (clampedValue != ValueInput .tag) {
615+ ValueInput .tag = clampedValue
616+ updateButtons(clampedValue)
617+ }
618+ }
619+ }
620+ }
621+ setOnFocusChangeListener { _, hasFocus ->
622+ if (hasFocus) {
623+ val currentValue = ValueInput .tag as ? Int ? : initialValue
624+ val text = currentValue.toString()
625+ ValueInput .setText(text)
626+ ValueInput .setSelection(text.length)
627+ context.showKeyboard(ValueInput )
519628 } else {
520- setAlpha(1f ) // Restore normal appearance
629+ val currentValue = ValueInput .tag as ? Int ? : initialValue
630+ ValueInput .setText(labelFormatter?.invoke(currentValue) ? : currentValue.toString())
631+ updateButtons(currentValue)
632+ onChange(currentValue)
633+ }
634+ }
635+ setOnEditorActionListener { v, actionId, _ ->
636+ if (actionId == EditorInfo .IME_ACTION_DONE || actionId == EditorInfo .IME_ACTION_NEXT ) {
637+ context.hideKeyboard(ValueInput )
638+ v.clearFocus() // This triggers the OnFocusChangeListener logic
639+ true
640+ } else {
641+ false
521642 }
522643 }
523644 }
524- setup(preference, context, value, onChange = onChange)
525645}
526646
527- fun PreferenceSeekbarBinding. setupAutoEmptyBin (
647+ fun PreferenceStepperBinding. setup (
528648 preference : IntPreference ,
529649 context : Context ,
530650 value : Int = preference.value,
651+ labelFormatter : ((Int ) -> String )? = null,
531652 onChange : (newValue: Int ) -> Unit ,
532653) {
533- Slider .apply {
534- setLabelFormatter { sliderValue ->
535- if (sliderValue == 0f ) {
536- context.getString(R .string.disabled)
537- } else " ${sliderValue.toInt()} ${context.getString(R .string.days)} "
538- }
654+ setup(
655+ value,
656+ preference.titleResId!! ,
657+ preference.min,
658+ preference.max,
659+ context,
660+ labelFormatter = labelFormatter,
661+ ) { newValue ->
662+ onChange(newValue)
539663 }
540- setup(preference, context, value, R .string.auto_remove_deleted_notes_hint, onChange)
541664}
542665
543666fun PreferenceBinding.setupStartView (
0 commit comments