diff --git a/app/build.gradle b/app/build.gradle index a4fc92c9f..674284caf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,7 +83,7 @@ dependencies { implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' - implementation 'com.google.android.material:material:1.5.0' + implementation 'com.google.android.material:material:1.6.0-alpha02' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1' diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableButtonGroup.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableButtonGroup.kt new file mode 100644 index 000000000..77d4acc2c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableButtonGroup.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.base.ui.widgets + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.annotation.AttrRes +import androidx.annotation.IdRes +import androidx.core.view.children +import com.google.android.material.button.MaterialButton + +class CheckableButtonGroup @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr), View.OnClickListener { + + var onCheckedChangeListener: OnCheckedChangeListener? = null + + override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { + if (child is MaterialButton) { + child.setOnClickListener(this) + } + super.addView(child, index, params) + } + + override fun onClick(v: View) { + setCheckedId(v.id) + } + + fun setCheckedId(@IdRes viewRes: Int) { + children.forEach { + (it as? MaterialButton)?.isChecked = it.id == viewRes + } + onCheckedChangeListener?.onCheckedChanged(this, viewRes) + } + + fun interface OnCheckedChangeListener { + fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt index dabe18a33..46dfaefd5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeSelectDialog.kt @@ -4,31 +4,25 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.SeekBar import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.slider.Slider import org.koin.android.ext.android.inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.databinding.DialogListModeBinding +import org.koitharu.kotatsu.utils.ext.setValueRounded +import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter -class ListModeSelectDialog : AlertDialogFragment(), View.OnClickListener, - SeekBar.OnSeekBarChangeListener { +class ListModeSelectDialog : AlertDialogFragment(), + CheckableButtonGroup.OnCheckedChangeListener, Slider.OnSliderTouchListener { private val settings by inject(mode = LazyThreadSafetyMode.NONE) - private var mode: ListMode = ListMode.GRID - private var pendingGridSize: Int = 100 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - mode = settings.listMode - pendingGridSize = settings.gridSize - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -42,51 +36,42 @@ class ListModeSelectDialog : AlertDialogFragment(), View. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val mode = settings.listMode binding.buttonList.isChecked = mode == ListMode.LIST binding.buttonListDetailed.isChecked = mode == ListMode.DETAILED_LIST binding.buttonGrid.isChecked = mode == ListMode.GRID binding.textViewGridTitle.isVisible = mode == ListMode.GRID - binding.seekbarGrid.isVisible = mode == ListMode.GRID + binding.sliderGrid.isVisible = mode == ListMode.GRID - with(binding.seekbarGrid) { - progress = pendingGridSize - 50 - setOnSeekBarChangeListener(this@ListModeSelectDialog) - } + binding.sliderGrid.setLabelFormatter(IntPercentLabelFormatter()) + binding.sliderGrid.setValueRounded(settings.gridSize.toFloat()) + binding.sliderGrid.addOnSliderTouchListener(this) - binding.buttonList.setOnClickListener(this) - binding.buttonGrid.setOnClickListener(this) - binding.buttonListDetailed.setOnClickListener(this) + binding.checkableGroup.onCheckedChangeListener = this } - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - pendingGridSize = progress + 50 - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - settings.gridSize = pendingGridSize - } - - override fun onClick(v: View) { - when (v.id) { - R.id.button_list -> mode = ListMode.LIST - R.id.button_list_detailed -> mode = ListMode.DETAILED_LIST - R.id.button_grid -> mode = ListMode.GRID + override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) { + val mode = when (checkedId) { + R.id.button_list -> ListMode.LIST + R.id.button_list_detailed -> ListMode.DETAILED_LIST + R.id.button_grid -> ListMode.GRID + else -> return } binding.textViewGridTitle.isVisible = mode == ListMode.GRID - binding.seekbarGrid.isVisible = mode == ListMode.GRID + binding.sliderGrid.isVisible = mode == ListMode.GRID settings.listMode = mode } + override fun onStartTrackingTouch(slider: Slider) = Unit + + override fun onStopTrackingTouch(slider: Slider) { + settings.gridSize = slider.value.toInt() + } + companion object { private const val TAG = "ListModeSelectDialog" - fun show(fm: FragmentManager) = ListModeSelectDialog() - .show( - fm, - TAG - ) + fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt index 8ef5ecd12..85f326c68 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderConfigDialog.kt @@ -9,12 +9,13 @@ import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.databinding.DialogReaderConfigBinding import org.koitharu.kotatsu.utils.ext.withArgs class ReaderConfigDialog : AlertDialogFragment(), - View.OnClickListener { + CheckableButtonGroup.OnCheckedChangeListener { private lateinit var mode: ReaderMode @@ -42,9 +43,7 @@ class ReaderConfigDialog : AlertDialogFragment(), binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON - binding.buttonStandard.setOnClickListener(this) - binding.buttonReversed.setOnClickListener(this) - binding.buttonWebtoon.setOnClickListener(this) + binding.checkableGroup.onCheckedChangeListener = this } override fun onDismiss(dialog: DialogInterface) { @@ -53,11 +52,12 @@ class ReaderConfigDialog : AlertDialogFragment(), super.onDismiss(dialog) } - override fun onClick(v: View) { - when (v.id) { - R.id.button_standard -> mode = ReaderMode.STANDARD - R.id.button_webtoon -> mode = ReaderMode.WEBTOON - R.id.button_reversed -> mode = ReaderMode.REVERSED + override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) { + mode = when (checkedId) { + R.id.button_standard -> ReaderMode.STANDARD + R.id.button_webtoon -> ReaderMode.WEBTOON + R.id.button_reversed -> ReaderMode.REVERSED + else -> return } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt index 557c291c7..1bc8be9fc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt @@ -8,7 +8,10 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.appcompat.app.AppCompatDelegate -import androidx.preference.* +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreference import leakcanary.LeakCanary import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment @@ -17,6 +20,7 @@ import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity +import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.names import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat @@ -35,7 +39,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_main) - findPreference(AppSettings.KEY_GRID_SIZE)?.run { + findPreference(AppSettings.KEY_GRID_SIZE)?.run { summary = "%d%%".format(value) setOnPreferenceChangeListener { preference, newValue -> preference.summary = "%d%%".format(newValue) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt new file mode 100644 index 000000000..472467a1d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt @@ -0,0 +1,155 @@ +package org.koitharu.kotatsu.settings.utils + +import android.content.Context +import android.content.res.TypedArray +import android.os.Parcel +import android.os.Parcelable +import android.util.AttributeSet +import android.view.View +import androidx.core.content.withStyledAttributes +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.google.android.material.slider.Slider +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.utils.ext.setValueRounded + +class SliderPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.sliderPreferenceStyle, + defStyleRes: Int = R.style.Preference_Slider, +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + private var valueFrom: Int = 0 + private var valueTo: Int = 100 + private var stepSize: Int = 1 + private var currentValue: Int = 0 + + var value: Int + get() = currentValue + set(value) = setValueInternal(value, notifyChanged = true) + + private val sliderListener = Slider.OnChangeListener { _, value, fromUser -> + if (fromUser) { + syncValueInternal(value.toInt()) + } + } + + init { + context.withStyledAttributes(attrs, + R.styleable.SliderPreference, + defStyleAttr, + defStyleRes) { + valueFrom = getFloat(R.styleable.SliderPreference_android_valueFrom, + valueFrom.toFloat()).toInt() + valueTo = + getFloat(R.styleable.SliderPreference_android_valueTo, valueTo.toFloat()).toInt() + stepSize = + getFloat(R.styleable.SliderPreference_android_stepSize, stepSize.toFloat()).toInt() + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + val slider = holder.findViewById(R.id.slider) as? Slider ?: return + slider.removeOnChangeListener(sliderListener) + slider.addOnChangeListener(sliderListener) + slider.valueFrom = valueFrom.toFloat() + slider.valueTo = valueTo.toFloat() + slider.stepSize = stepSize.toFloat() + slider.setValueRounded(currentValue.toFloat()) + slider.isEnabled = isEnabled + } + + override fun onSetInitialValue(defaultValue: Any?) { + value = getPersistedInt(defaultValue as? Int ?: 0) + } + + override fun onGetDefaultValue(a: TypedArray, index: Int): Any { + return a.getInt(index, 0) + } + + override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() + if (superState == null || isPersistent) { + return superState + } + return SavedState( + superState = superState, + valueFrom = valueFrom, + valueTo = valueTo, + currentValue = currentValue, + ) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state !is SavedState) { + super.onRestoreInstanceState(state) + return + } + super.onRestoreInstanceState(state.superState) + valueFrom = state.valueFrom + valueTo = state.valueTo + currentValue = state.currentValue + notifyChanged() + } + + private fun setValueInternal(sliderValue: Int, notifyChanged: Boolean) { + val newValue = sliderValue.coerceIn(valueFrom, valueTo) + if (newValue != currentValue) { + currentValue = newValue + persistInt(newValue) + if (notifyChanged) { + notifyChanged() + } + } + } + + private fun syncValueInternal(sliderValue: Int) { + if (sliderValue != currentValue) { + if (callChangeListener(sliderValue)) { + setValueInternal(sliderValue, notifyChanged = false) + } + } + } + + private class SavedState : View.BaseSavedState { + + val valueFrom: Int + val valueTo: Int + val currentValue: Int + + constructor( + superState: Parcelable, + valueFrom: Int, + valueTo: Int, + currentValue: Int, + ) : super(superState) { + this.valueFrom = valueFrom + this.valueTo = valueTo + this.currentValue = currentValue + } + + constructor(source: Parcel) : super(source) { + valueFrom = source.readInt() + valueTo = source.readInt() + currentValue = source.readInt() + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + out.writeInt(valueFrom) + out.writeInt(valueTo) + out.writeInt(currentValue) + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(`in`: Parcel) = SavedState(`in`) + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index 2bd9d2e49..95546d3c2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -18,7 +18,9 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.progressindicator.BaseProgressIndicator +import com.google.android.material.slider.Slider import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder +import kotlin.math.roundToInt fun View.hideKeyboard() { val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager @@ -200,4 +202,9 @@ fun resolveAdjustedSize( // This should not happen desiredSize } +} + +fun Slider.setValueRounded(newValue: Float) { + val step = stepSize + value = (newValue / step).roundToInt() * step } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/IntPercentLabelFormatter.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/IntPercentLabelFormatter.kt new file mode 100644 index 000000000..ed9773c99 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/IntPercentLabelFormatter.kt @@ -0,0 +1,7 @@ +package org.koitharu.kotatsu.utils.progress + +import com.google.android.material.slider.LabelFormatter + +class IntPercentLabelFormatter : LabelFormatter { + override fun getFormattedValue(value: Float) = "%d%%".format(value.toInt()) +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_list_mode.xml b/app/src/main/res/layout/dialog_list_mode.xml index 8fb04828e..cfbd8e018 100644 --- a/app/src/main/res/layout/dialog_list_mode.xml +++ b/app/src/main/res/layout/dialog_list_mode.xml @@ -8,13 +8,12 @@ android:animateLayoutChanges="true" android:orientation="vertical"> - + android:orientation="vertical"> - + - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_reader_config.xml b/app/src/main/res/layout/dialog_reader_config.xml index 37ba52685..ab4e84e67 100644 --- a/app/src/main/res/layout/dialog_reader_config.xml +++ b/app/src/main/res/layout/dialog_reader_config.xml @@ -4,15 +4,15 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:padding="16dp"> + android:paddingHorizontal="16dp" + android:orientation="vertical"> - + android:layout_marginTop="16dp" + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ff8094f6b..9b721e46c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,6 +1,8 @@ + + @@ -12,4 +14,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 27c27a4e9..4ca1bf269 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,4 +1,4 @@ - + @@ -58,11 +58,11 @@ - @@ -136,4 +136,8 @@ false + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 21743702c..84bae3f2a 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -45,16 +45,14 @@ app:iconSpaceReserved="false" app:useSimpleSummaryProvider="true" /> - + app:iconSpaceReserved="false" />