Refactor sources settings list
This commit is contained in:
@@ -92,7 +92,6 @@ class KotatsuApp : Application() {
|
|||||||
.detectFragmentReuse()
|
.detectFragmentReuse()
|
||||||
.detectWrongFragmentContainer()
|
.detectWrongFragmentContainer()
|
||||||
.detectRetainInstanceUsage()
|
.detectRetainInstanceUsage()
|
||||||
.detectTargetFragmentUsage()
|
|
||||||
.detectSetUserVisibleHint()
|
.detectSetUserVisibleHint()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ object MangaProviderFactory {
|
|||||||
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
|
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
|
||||||
val list = MangaSource.values().toList() - MangaSource.LOCAL
|
val list = MangaSource.values().toList() - MangaSource.LOCAL
|
||||||
val order = settings.sourcesOrder
|
val order = settings.sourcesOrder
|
||||||
val hidden = settings.hiddenSources
|
|
||||||
val sorted = list.sortedBy { x ->
|
val sorted = list.sortedBy { x ->
|
||||||
val e = order.indexOf(x.ordinal)
|
val e = order.indexOf(x.ordinal)
|
||||||
if (e == -1) order.size + x.ordinal else e
|
if (e == -1) order.size + x.ordinal else e
|
||||||
@@ -16,6 +15,7 @@ object MangaProviderFactory {
|
|||||||
return if (includeHidden) {
|
return if (includeHidden) {
|
||||||
sorted
|
sorted
|
||||||
} else {
|
} else {
|
||||||
|
val hidden = settings.hiddenSources
|
||||||
sorted.filterNot { x ->
|
sorted.filterNot { x ->
|
||||||
x.name in hidden
|
x.name in hidden
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
abstract class BaseViewHolder<T, E, B : ViewBinding> protected constructor(val binding: B) :
|
|
||||||
RecyclerView.ViewHolder(binding.root), KoinComponent {
|
|
||||||
|
|
||||||
var boundData: T? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
val context get() = itemView.context!!
|
|
||||||
|
|
||||||
fun bind(data: T, extra: E) {
|
|
||||||
boundData = data
|
|
||||||
onBind(data, extra)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requireData(): T {
|
|
||||||
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onRecycled() = Unit
|
|
||||||
|
|
||||||
abstract fun onBind(data: T, extra: E)
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list.decor
|
|
||||||
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.utils.ext.inflate
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://github.com/paetztm/recycler_view_headers
|
|
||||||
*/
|
|
||||||
class SectionItemDecoration(
|
|
||||||
private val isSticky: Boolean,
|
|
||||||
private val callback: Callback
|
|
||||||
) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private var headerView: TextView? = null
|
|
||||||
private var headerOffset: Int = 0
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State
|
|
||||||
) {
|
|
||||||
if (headerOffset == 0) {
|
|
||||||
headerOffset = parent.resources.getDimensionPixelSize(R.dimen.header_height)
|
|
||||||
}
|
|
||||||
val pos = parent.getChildAdapterPosition(view)
|
|
||||||
outRect.set(0, if (callback.isSection(pos)) headerOffset else 0, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
||||||
super.onDrawOver(c, parent, state)
|
|
||||||
val textView = headerView ?: parent.inflate<TextView>(R.layout.item_filter_header).also {
|
|
||||||
headerView = it
|
|
||||||
}
|
|
||||||
fixLayoutSize(textView, parent)
|
|
||||||
|
|
||||||
for (child in parent.children) {
|
|
||||||
val pos = parent.getChildAdapterPosition(child)
|
|
||||||
if (callback.isSection(pos)) {
|
|
||||||
textView.text = callback.getSectionTitle(pos) ?: continue
|
|
||||||
c.save()
|
|
||||||
if (isSticky) {
|
|
||||||
c.translate(
|
|
||||||
0f,
|
|
||||||
max(0f, (child.top - textView.height).toFloat())
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
c.translate(
|
|
||||||
0f,
|
|
||||||
(child.top - textView.height).toFloat()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
textView.draw(c)
|
|
||||||
c.restore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the header view to make sure its size is greater than 0 and will be drawn
|
|
||||||
* https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations
|
|
||||||
*/
|
|
||||||
private fun fixLayoutSize(view: View, parent: ViewGroup) {
|
|
||||||
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
|
|
||||||
val heightSpec =
|
|
||||||
View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
|
|
||||||
|
|
||||||
val childWidth = ViewGroup.getChildMeasureSpec(
|
|
||||||
widthSpec,
|
|
||||||
parent.paddingLeft + parent.paddingRight,
|
|
||||||
view.layoutParams.width
|
|
||||||
)
|
|
||||||
val childHeight = ViewGroup.getChildMeasureSpec(
|
|
||||||
heightSpec,
|
|
||||||
parent.paddingTop + parent.paddingBottom,
|
|
||||||
view.layoutParams.height
|
|
||||||
)
|
|
||||||
view.measure(childWidth, childHeight)
|
|
||||||
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
|
|
||||||
fun isSection(position: Int): Boolean
|
|
||||||
|
|
||||||
fun getSectionTitle(position: Int): CharSequence?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,6 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun onPreferenceStartFragment(
|
override fun onPreferenceStartFragment(
|
||||||
caller: PreferenceFragmentCompat,
|
caller: PreferenceFragmentCompat,
|
||||||
pref: Preference
|
pref: Preference
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.settings.backup.BackupViewModel
|
|||||||
import org.koitharu.kotatsu.settings.backup.RestoreViewModel
|
import org.koitharu.kotatsu.settings.backup.RestoreViewModel
|
||||||
import org.koitharu.kotatsu.settings.onboard.OnboardViewModel
|
import org.koitharu.kotatsu.settings.onboard.OnboardViewModel
|
||||||
import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel
|
import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel
|
||||||
|
import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel
|
||||||
|
|
||||||
val settingsModule
|
val settingsModule
|
||||||
get() = module {
|
get() = module {
|
||||||
@@ -25,4 +26,5 @@ val settingsModule
|
|||||||
}
|
}
|
||||||
viewModel { ProtectSetupViewModel(get()) }
|
viewModel { ProtectSetupViewModel(get()) }
|
||||||
viewModel { OnboardViewModel(get()) }
|
viewModel { OnboardViewModel(get()) }
|
||||||
|
viewModel { SourcesSettingsViewModel(get()) }
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
|||||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||||
import org.koitharu.kotatsu.utils.ext.map
|
import org.koitharu.kotatsu.utils.ext.map
|
||||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||||
|
import org.koitharu.kotatsu.utils.ext.toTitleCase
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class OnboardViewModel(
|
class OnboardViewModel(
|
||||||
@@ -27,9 +28,9 @@ class OnboardViewModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
if (settings.isSourcesSelected) {
|
if (settings.isSourcesSelected) {
|
||||||
selectedLocales.removeAll(settings.hiddenSources.map { x -> MangaSource.valueOf(x).locale })
|
selectedLocales.removeAll(settings.hiddenSources.mapToSet { x -> MangaSource.valueOf(x).locale })
|
||||||
} else {
|
} else {
|
||||||
val deviceLocales = LocaleListCompat.getDefault().map { x ->
|
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
|
||||||
x.language
|
x.language
|
||||||
}
|
}
|
||||||
selectedLocales.retainAll(deviceLocales)
|
selectedLocales.retainAll(deviceLocales)
|
||||||
@@ -64,7 +65,7 @@ class OnboardViewModel(
|
|||||||
} else null
|
} else null
|
||||||
SourceLocale(
|
SourceLocale(
|
||||||
key = key,
|
key = key,
|
||||||
title = locale?.getDisplayLanguage(locale)?.capitalize(locale),
|
title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale),
|
||||||
isChecked = key in selectedLocales
|
isChecked = key in selectedLocales
|
||||||
)
|
)
|
||||||
}.sortedWith(SourceLocaleComparator())
|
}.sortedWith(SourceLocaleComparator())
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
|
|
||||||
|
|
||||||
class SourceViewHolder(parent: ViewGroup) :
|
|
||||||
BaseViewHolder<MangaSource, Boolean, ItemSourceConfigBinding>(
|
|
||||||
ItemSourceConfigBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun onBind(data: MangaSource, extra: Boolean) {
|
|
||||||
binding.textViewTitle.text = data.title
|
|
||||||
binding.switchToggle.isChecked = extra
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
|
||||||
|
|
||||||
class SourcesAdapter(
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val onItemClickListener: OnListItemClickListener<MangaSource>,
|
|
||||||
) : RecyclerView.Adapter<SourceViewHolder>() {
|
|
||||||
|
|
||||||
private val dataSet =
|
|
||||||
MangaProviderFactory.getSources(settings, includeHidden = true).toMutableList()
|
|
||||||
private val hiddenItems = settings.hiddenSources.mapNotNull {
|
|
||||||
runCatching {
|
|
||||||
MangaSource.valueOf(it)
|
|
||||||
}.getOrNull()
|
|
||||||
}.toMutableSet()
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
) = SourceViewHolder(parent).also(::onViewHolderCreated)
|
|
||||||
|
|
||||||
override fun getItemCount() = dataSet.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
|
|
||||||
val item = dataSet[position]
|
|
||||||
holder.bind(item, !hiddenItems.contains(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private fun onViewHolderCreated(holder: SourceViewHolder) {
|
|
||||||
holder.binding.switchToggle.setOnCheckedChangeListener { _, it ->
|
|
||||||
if (it) {
|
|
||||||
hiddenItems.remove(holder.requireData())
|
|
||||||
} else {
|
|
||||||
hiddenItems.add(holder.requireData())
|
|
||||||
}
|
|
||||||
settings.hiddenSources = hiddenItems.mapToSet { x -> x.name }
|
|
||||||
}
|
|
||||||
holder.binding.imageViewConfig.setOnClickListener { v ->
|
|
||||||
onItemClickListener.onItemClick(holder.requireData(), v)
|
|
||||||
}
|
|
||||||
holder.binding.imageViewHandle.setOnTouchListener { v, event ->
|
|
||||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
||||||
onItemClickListener.onItemLongClick(
|
|
||||||
holder.requireData(),
|
|
||||||
holder.itemView
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun moveItem(oldPos: Int, newPos: Int) {
|
|
||||||
val item = dataSet.removeAt(oldPos)
|
|
||||||
dataSet.add(newPos, item)
|
|
||||||
notifyItemMoved(oldPos, newPos)
|
|
||||||
settings.sourcesOrder = dataSet.map { it.ordinal }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class SourcesReorderCallback :
|
|
||||||
ItemTouchHelper.SimpleCallback(ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0) {
|
|
||||||
|
|
||||||
override fun onMove(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
|
||||||
target: RecyclerView.ViewHolder
|
|
||||||
): Boolean {
|
|
||||||
val adapter = recyclerView.adapter as? SourcesAdapter ?: return false
|
|
||||||
val oldPos = viewHolder.bindingAdapterPosition
|
|
||||||
val newPos = target.bindingAdapterPosition
|
|
||||||
adapter.moveItem(oldPos, newPos)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
|
|
||||||
|
|
||||||
override fun isLongPressDragEnabled() = false
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,28 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources
|
package org.koitharu.kotatsu.settings.sources
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
|
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
|
||||||
|
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigItem
|
||||||
|
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
||||||
|
|
||||||
class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
||||||
OnListItemClickListener<MangaSource> {
|
SourceConfigListener {
|
||||||
|
|
||||||
private lateinit var reorderHelper: ItemTouchHelper
|
private lateinit var reorderHelper: ItemTouchHelper
|
||||||
|
private val viewModel by viewModel<SourcesSettingsViewModel>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -39,11 +42,16 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val sourcesAdapter = SourceConfigAdapter(this)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
|
setHasFixedSize(true)
|
||||||
addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
|
addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
|
||||||
adapter = SourcesAdapter(get(), this@SourcesSettingsFragment)
|
adapter = sourcesAdapter
|
||||||
reorderHelper.attachToRecyclerView(this)
|
reorderHelper.attachToRecyclerView(this)
|
||||||
}
|
}
|
||||||
|
viewModel.items.observe(viewLifecycleOwner) {
|
||||||
|
sourcesAdapter.items = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -51,22 +59,6 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
// TODO handle changes in dialog
|
|
||||||
// inflater.inflate(R.menu.opt_sources, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
return when(item.itemId) {
|
|
||||||
R.id.action_languages -> {
|
|
||||||
OnboardDialogFragment.show(parentFragmentManager)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
binding.recyclerView.updatePadding(
|
binding.recyclerView.updatePadding(
|
||||||
bottom = insets.bottom,
|
bottom = insets.bottom,
|
||||||
@@ -75,14 +67,43 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: MangaSource, view: View) {
|
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) {
|
||||||
(activity as? SettingsActivity)?.openMangaSourceSettings(item)
|
(activity as? SettingsActivity)?.openMangaSourceSettings(item.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: MangaSource, view: View): Boolean {
|
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
||||||
reorderHelper.startDrag(
|
viewModel.setEnabled(item.source, isEnabled)
|
||||||
binding.recyclerView.findContainingViewHolder(view) ?: return false
|
}
|
||||||
)
|
|
||||||
return true
|
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) {
|
||||||
|
reorderHelper.startDrag(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHeaderClick(header: SourceConfigItem.LocaleHeader) {
|
||||||
|
viewModel.expandOrCollapse(header.localeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
|
||||||
|
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
|
||||||
|
0,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
if (viewHolder.itemViewType != target.itemViewType) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val oldPos = viewHolder.bindingAdapterPosition
|
||||||
|
val newPos = target.bindingAdapterPosition
|
||||||
|
viewModel.reorderSources(oldPos, newPos)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
|
||||||
|
|
||||||
|
override fun isLongPressDragEnabled() = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigItem
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class SourcesSettingsViewModel(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
val items = MutableLiveData<List<SourceConfigItem>>(emptyList())
|
||||||
|
private val expandedGroups = HashSet<String?>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reorderSources(oldPos: Int, newPos: Int) {
|
||||||
|
val snapshot = items.value?.toMutableList() ?: return
|
||||||
|
Collections.swap(snapshot, oldPos, newPos)
|
||||||
|
settings.sourcesOrder = snapshot.mapNotNull {
|
||||||
|
(it as? SourceConfigItem.SourceItem)?.source?.ordinal
|
||||||
|
}
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEnabled(source: MangaSource, isEnabled: Boolean) {
|
||||||
|
settings.hiddenSources = if (isEnabled) {
|
||||||
|
settings.hiddenSources - source.name
|
||||||
|
} else {
|
||||||
|
settings.hiddenSources + source.name
|
||||||
|
}
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun expandOrCollapse(headerId: String?) {
|
||||||
|
if (headerId in expandedGroups) {
|
||||||
|
expandedGroups.remove(headerId)
|
||||||
|
} else {
|
||||||
|
expandedGroups.add(headerId)
|
||||||
|
}
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildList() {
|
||||||
|
val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
|
||||||
|
val hiddenSources = settings.hiddenSources
|
||||||
|
items.value = sources.map {
|
||||||
|
SourceConfigItem.SourceItem(
|
||||||
|
source = it,
|
||||||
|
isEnabled = it.name !in hiddenSources,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.adapter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
|
||||||
|
class SourceConfigAdapter(
|
||||||
|
listener: SourceConfigListener,
|
||||||
|
) : AsyncListDifferDelegationAdapter<SourceConfigItem>(
|
||||||
|
SourceConfigDiffCallback(),
|
||||||
|
sourceConfigHeaderDelegate(listener),
|
||||||
|
sourceConfigItemDelegate(listener),
|
||||||
|
)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.adapter
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemExpandableBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
|
||||||
|
|
||||||
|
fun sourceConfigHeaderDelegate(
|
||||||
|
listener: SourceConfigListener,
|
||||||
|
) = adapterDelegateViewBinding<SourceConfigItem.LocaleHeader, SourceConfigItem, ItemExpandableBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
listener.onHeaderClick(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.root.text = item.title ?: getString(R.string.other)
|
||||||
|
binding.root.isChecked = item.isExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
fun sourceConfigItemDelegate(
|
||||||
|
listener: SourceConfigListener,
|
||||||
|
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
val eventListener = object : View.OnClickListener, View.OnTouchListener,
|
||||||
|
CompoundButton.OnCheckedChangeListener {
|
||||||
|
override fun onClick(v: View?) = listener.onItemSettingsClick(item)
|
||||||
|
|
||||||
|
override fun onTouch(v: View?, event: MotionEvent): Boolean {
|
||||||
|
return if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
|
listener.onDragHandleTouch(this@adapterDelegateViewBinding)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
|
||||||
|
listener.onItemEnabledChanged(item, isChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.imageViewConfig.setOnClickListener(eventListener)
|
||||||
|
binding.switchToggle.setOnCheckedChangeListener(eventListener)
|
||||||
|
binding.imageViewHandle.setOnTouchListener(eventListener)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = item.source.title
|
||||||
|
binding.switchToggle.isChecked = item.isEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.adapter
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
|
||||||
|
class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem.javaClass != newItem.javaClass -> false
|
||||||
|
oldItem is SourceConfigItem.LocaleHeader && newItem is SourceConfigItem.LocaleHeader -> {
|
||||||
|
oldItem.localeId == newItem.localeId
|
||||||
|
}
|
||||||
|
oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> {
|
||||||
|
oldItem.source == newItem.source
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem is SourceConfigItem.LocaleHeader && newItem is SourceConfigItem.LocaleHeader -> {
|
||||||
|
oldItem.title == newItem.title && oldItem.isExpanded == newItem.isExpanded
|
||||||
|
}
|
||||||
|
oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> {
|
||||||
|
oldItem.isEnabled == newItem.isEnabled
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItem: SourceConfigItem, newItem: SourceConfigItem) = Unit
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.adapter
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
|
||||||
|
sealed interface SourceConfigItem {
|
||||||
|
|
||||||
|
data class LocaleHeader(
|
||||||
|
val localeId: String?,
|
||||||
|
val title: String?,
|
||||||
|
val isExpanded: Boolean,
|
||||||
|
) : SourceConfigItem
|
||||||
|
|
||||||
|
data class SourceItem(
|
||||||
|
val source: MangaSource,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
) : SourceConfigItem
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.adapter
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
interface SourceConfigListener {
|
||||||
|
|
||||||
|
fun onItemSettingsClick(item: SourceConfigItem.SourceItem)
|
||||||
|
|
||||||
|
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
|
||||||
|
|
||||||
|
fun onDragHandleTouch(holder: RecyclerView.ViewHolder)
|
||||||
|
|
||||||
|
fun onHeaderClick(header: SourceConfigItem.LocaleHeader)
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.utils.ext
|
|||||||
|
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
fun LocaleListCompat.toList(): List<Locale> {
|
fun LocaleListCompat.toList(): List<Locale> {
|
||||||
val list = ArrayList<Locale>(size())
|
val list = ArrayList<Locale>(size())
|
||||||
@@ -26,4 +25,8 @@ inline fun <R, C : MutableCollection<in R>> LocaleListCompat.mapTo(
|
|||||||
|
|
||||||
inline fun <T> LocaleListCompat.map(block: (Locale) -> T): List<T> {
|
inline fun <T> LocaleListCompat.map(block: (Locale) -> T): List<T> {
|
||||||
return mapTo(ArrayList(size()), block)
|
return mapTo(ArrayList(size()), block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> LocaleListCompat.mapToSet(block: (Locale) -> T): Set<T> {
|
||||||
|
return mapTo(LinkedHashSet(size()), block)
|
||||||
}
|
}
|
||||||
5
app/src/main/res/drawable/ic_expand_collapse.xml
Normal file
5
app/src/main/res/drawable/ic_expand_collapse.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_expand_less" android:state_checked="true" />
|
||||||
|
<item android:drawable="@drawable/ic_expand_more" android:state_checked="false" />
|
||||||
|
</selector>
|
||||||
5
app/src/main/res/drawable/ic_expand_less.xml
Normal file
5
app/src/main/res/drawable/ic_expand_less.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_expand_more.xml
Normal file
11
app/src/main/res/drawable/ic_expand_more.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
|
||||||
|
</vector>
|
||||||
15
app/src/main/res/layout/item_expandable.xml
Normal file
15
app/src/main/res/layout/item_expandable.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<CheckedTextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:drawablePadding="12dp"
|
||||||
|
android:gravity="center_vertical|start"
|
||||||
|
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
app:drawableEndCompat="@drawable/ic_expand_collapse"
|
||||||
|
app:drawableTint="?android:textColorPrimary"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
Reference in New Issue
Block a user