Configurable main navigation
This commit is contained in:
@@ -23,7 +23,6 @@ import org.koitharu.kotatsu.core.util.ext.map
|
||||
import org.koitharu.kotatsu.core.util.ext.postDelayed
|
||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.parsers.util.names
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
|
||||
@@ -67,6 +66,7 @@ class AppearanceSettingsFragment :
|
||||
}
|
||||
setDefaultValueCompat("")
|
||||
}
|
||||
bindNavSummary()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -86,7 +86,8 @@ class AppearanceSettingsFragment :
|
||||
}
|
||||
|
||||
AppSettings.KEY_COLOR_THEME,
|
||||
AppSettings.KEY_THEME_AMOLED -> {
|
||||
AppSettings.KEY_THEME_AMOLED,
|
||||
-> {
|
||||
postRestart()
|
||||
}
|
||||
|
||||
@@ -94,8 +95,8 @@ class AppearanceSettingsFragment :
|
||||
AppCompatDelegate.setApplicationLocales(settings.appLocales)
|
||||
}
|
||||
|
||||
AppSettings.KEY_FIRST_NAV_ITEM -> {
|
||||
activityRecreationHandle.recreate(MainActivity::class.java)
|
||||
AppSettings.KEY_NAV_MAIN -> {
|
||||
bindNavSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +128,13 @@ class AppearanceSettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindNavSummary() {
|
||||
val pref = findPreference<Preference>(AppSettings.KEY_NAV_MAIN) ?: return
|
||||
pref.summary = settings.mainNavItems.joinToString {
|
||||
getString(it.title)
|
||||
}
|
||||
}
|
||||
|
||||
private class LocaleComparator(context: Context) : Comparator<Locale> {
|
||||
|
||||
private val deviceLocales = LocaleManagerCompat.getSystemLocales(context)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.koitharu.kotatsu.settings.nav
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.NavItem
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.settings.nav.adapter.navAddAD
|
||||
import org.koitharu.kotatsu.settings.nav.adapter.navAvailableAD
|
||||
import org.koitharu.kotatsu.settings.nav.adapter.navConfigAD
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NavConfigFragment : BaseFragment<FragmentSettingsSourcesBinding>(), RecyclerViewOwner,
|
||||
OnListItemClickListener<NavItem>, View.OnClickListener {
|
||||
|
||||
private var reorderHelper: ItemTouchHelper? = null
|
||||
private val viewModel by viewModels<NavConfigViewModel>()
|
||||
|
||||
override val recyclerView: RecyclerView
|
||||
get() = requireViewBinding().recyclerView
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
): FragmentSettingsSourcesBinding {
|
||||
return FragmentSettingsSourcesBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(
|
||||
binding: FragmentSettingsSourcesBinding,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
val navConfigAdapter = BaseListAdapter<ListModel>()
|
||||
.addDelegate(ListItemType.NAV_ITEM, navConfigAD(this))
|
||||
.addDelegate(ListItemType.FOOTER_LOADING, navAddAD(this))
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = navConfigAdapter
|
||||
reorderHelper = ItemTouchHelper(ReorderCallback()).also {
|
||||
it.attachToRecyclerView(this)
|
||||
}
|
||||
}
|
||||
viewModel.content.observe(viewLifecycleOwner, navConfigAdapter)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.setTitle(R.string.main_screen_sections)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
reorderHelper = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
requireViewBinding().recyclerView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
var dialog: DialogInterface? = null
|
||||
val listener = OnListItemClickListener<NavItem> { item, _ ->
|
||||
viewModel.addItem(item)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
dialog = RecyclerViewAlertDialog.Builder<NavItem>(v.context)
|
||||
.setTitle(R.string.add)
|
||||
.addAdapterDelegate(navAvailableAD(listener))
|
||||
.setCancelable(true)
|
||||
.setItems(viewModel.availableItems)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
.apply { show() }
|
||||
}
|
||||
|
||||
override fun onItemClick(item: NavItem, view: View) {
|
||||
viewModel.removeItem(item)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: NavItem, view: View): Boolean {
|
||||
val holder = viewBinding?.recyclerView?.findContainingViewHolder(view) ?: return false
|
||||
reorderHelper?.startDrag(holder)
|
||||
return true
|
||||
}
|
||||
|
||||
private inner class ReorderCallback : ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
|
||||
0,
|
||||
) {
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder,
|
||||
): Boolean = true
|
||||
|
||||
override fun onMoved(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
fromPos: Int,
|
||||
target: RecyclerView.ViewHolder,
|
||||
toPos: Int,
|
||||
x: Int,
|
||||
y: Int,
|
||||
) {
|
||||
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
|
||||
viewModel.reorder(fromPos, toPos)
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
|
||||
|
||||
override fun isLongPressDragEnabled() = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.koitharu.kotatsu.settings.nav
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.NavItem
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.parsers.util.move
|
||||
import org.koitharu.kotatsu.settings.nav.model.NavItemAddModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NavConfigViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val activityRecreationHandle: ActivityRecreationHandle,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val items = MutableStateFlow(settings.mainNavItems)
|
||||
|
||||
val content: StateFlow<List<ListModel>> = items.map { snapshot ->
|
||||
if (snapshot.size < NavItem.entries.size) {
|
||||
snapshot + NavItemAddModel(snapshot.size < 5)
|
||||
} else {
|
||||
snapshot
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope + Dispatchers.Default,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
emptyList()
|
||||
)
|
||||
|
||||
private var commitJob: Job? = null
|
||||
|
||||
val availableItems
|
||||
get() = items.value.let { snapshot ->
|
||||
NavItem.entries.filterNot { x -> x in snapshot }
|
||||
}
|
||||
|
||||
fun reorder(fromPos: Int, toPos: Int) {
|
||||
items.value = items.value.toMutableList().apply {
|
||||
move(fromPos, toPos)
|
||||
commit(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun addItem(item: NavItem) {
|
||||
items.value = items.value.plus(item).also {
|
||||
commit(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeItem(item: NavItem) {
|
||||
items.value = items.value.minus(item).also {
|
||||
commit(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun commit(value: List<NavItem>) {
|
||||
val prevJob = commitJob
|
||||
commitJob = launchJob {
|
||||
prevJob?.cancelAndJoin()
|
||||
delay(500)
|
||||
settings.mainNavItems = value
|
||||
activityRecreationHandle.recreate(MainActivity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.koitharu.kotatsu.settings.nav.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.NavItem
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.databinding.ItemNavAvailableBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemNavConfigBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.settings.nav.model.NavItemAddModel
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun navConfigAD(
|
||||
clickListener: OnListItemClickListener<NavItem>,
|
||||
) = adapterDelegateViewBinding<NavItem, ListModel, ItemNavConfigBinding>(
|
||||
{ layoutInflater, parent -> ItemNavConfigBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
val eventListener = object : View.OnClickListener, View.OnTouchListener {
|
||||
override fun onClick(v: View) = clickListener.onItemClick(item, v)
|
||||
|
||||
override fun onTouch(v: View?, event: MotionEvent): Boolean =
|
||||
event.actionMasked == MotionEvent.ACTION_DOWN &&
|
||||
clickListener.onItemLongClick(item, itemView)
|
||||
}
|
||||
binding.imageViewRemove.setOnClickListener(eventListener)
|
||||
binding.imageViewReorder.setOnTouchListener(eventListener)
|
||||
|
||||
bind {
|
||||
with(binding.textViewTitle) {
|
||||
setText(item.title)
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navAvailableAD(
|
||||
clickListener: OnListItemClickListener<NavItem>,
|
||||
) = adapterDelegateViewBinding<NavItem, NavItem, ItemNavAvailableBinding>(
|
||||
{ layoutInflater, parent -> ItemNavAvailableBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.root.setOnClickListener { v ->
|
||||
clickListener.onItemClick(item, v)
|
||||
}
|
||||
|
||||
bind {
|
||||
with(binding.root) {
|
||||
setText(item.title)
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navAddAD(
|
||||
clickListener: View.OnClickListener,
|
||||
) = adapterDelegateViewBinding<NavItemAddModel, ListModel, ItemNavAvailableBinding>(
|
||||
{ layoutInflater, parent -> ItemNavAvailableBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.root.setOnClickListener(clickListener)
|
||||
binding.root.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_add, 0, 0, 0)
|
||||
|
||||
bind {
|
||||
with(binding.root) {
|
||||
setText(if (item.canAdd) R.string.add else R.string.items_limit_exceeded)
|
||||
isEnabled = item.canAdd
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.settings.nav.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class NavItemAddModel(
|
||||
val canAdd: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean = other is NavItemAddModel
|
||||
}
|
||||
Reference in New Issue
Block a user