diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractDividerItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractDividerItemDecoration.kt new file mode 100644 index 000000000..154303a91 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractDividerItemDecoration.kt @@ -0,0 +1,65 @@ +package org.koitharu.kotatsu.base.ui.list.decor + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.view.View +import androidx.core.view.children +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.utils.ext.getThemeDrawable +import kotlin.math.roundToInt + +abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { + + private val bounds = Rect() + private val divider = context.getThemeDrawable(android.R.attr.listDivider) + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State, + ) { + outRect.set(0, divider?.intrinsicHeight ?: 0, 0, 0) + } + + // TODO implement for horizontal lists on demand + override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) { + if (parent.layoutManager == null || divider == null) { + return + } + canvas.save() + val left: Int + val right: Int + if (parent.clipToPadding) { + left = parent.paddingLeft + right = parent.width - parent.paddingRight + canvas.clipRect( + left, parent.paddingTop, right, + parent.height - parent.paddingBottom + ) + } else { + left = 0 + right = parent.width + } + + var previous: RecyclerView.ViewHolder? = null + for (child in parent.children) { + val holder = parent.getChildViewHolder(child) + if (previous != null && shouldDrawDivider(previous, holder)) { + parent.getDecoratedBoundsWithMargins(child, bounds) + val top: Int = bounds.top + child.translationY.roundToInt() + val bottom: Int = top + divider.intrinsicHeight + divider.setBounds(left, top, right, bottom) + divider.draw(canvas) + } + previous = holder + } + canvas.restore() + } + + protected abstract fun shouldDrawDivider( + above: RecyclerView.ViewHolder, + below: RecyclerView.ViewHolder, + ): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index df04d68f8..25893f670 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -6,7 +6,6 @@ import android.view.View import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.updatePadding -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import org.koin.androidx.viewmodel.ext.android.viewModel @@ -15,8 +14,9 @@ import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter -import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigItem +import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigItemDecoration import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener +import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem class SourcesSettingsFragment : BaseFragment(), SourceConfigListener { @@ -45,7 +45,7 @@ class SourcesSettingsFragment : BaseFragment(), val sourcesAdapter = SourceConfigAdapter(this) with(binding.recyclerView) { setHasFixedSize(true) - addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL)) + addItemDecoration(SourceConfigItemDecoration(view.context)) adapter = sourcesAdapter reorderHelper.attachToRecyclerView(this) } @@ -79,7 +79,7 @@ class SourcesSettingsFragment : BaseFragment(), reorderHelper.startDrag(holder) } - override fun onHeaderClick(header: SourceConfigItem.LocaleHeader) { + override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) { viewModel.expandOrCollapse(header.localeId) } @@ -91,16 +91,20 @@ class SourcesSettingsFragment : BaseFragment(), 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 - } + target: RecyclerView.ViewHolder, + ): Boolean = viewHolder.itemViewType == target.itemViewType && viewModel.reorderSources( + viewHolder.bindingAdapterPosition, + target.bindingAdapterPosition, + ) + + override fun canDropOver( + recyclerView: RecyclerView, + current: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean = current.itemViewType == target.itemViewType && viewModel.canReorder( + current.bindingAdapterPosition, + target.bindingAdapterPosition, + ) override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt index b9eb5af40..52125df63 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt @@ -1,13 +1,20 @@ package org.koitharu.kotatsu.settings.sources +import androidx.core.os.LocaleListCompat import androidx.lifecycle.MutableLiveData +import org.koitharu.kotatsu.R 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 org.koitharu.kotatsu.settings.sources.model.SourceConfigItem +import org.koitharu.kotatsu.utils.ext.map +import org.koitharu.kotatsu.utils.ext.move +import org.koitharu.kotatsu.utils.ext.toTitleCase import java.util.* +private const val KEY_ENABLED = "!" + class SourcesSettingsViewModel( private val settings: AppSettings, ) : BaseViewModel() { @@ -19,13 +26,23 @@ class SourcesSettingsViewModel( buildList() } - fun reorderSources(oldPos: Int, newPos: Int) { - val snapshot = items.value?.toMutableList() ?: return - Collections.swap(snapshot, oldPos, newPos) + fun reorderSources(oldPos: Int, newPos: Int): Boolean { + val snapshot = items.value?.toMutableList() ?: return false + if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false + if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false + snapshot.move(oldPos, newPos) settings.sourcesOrder = snapshot.mapNotNull { (it as? SourceConfigItem.SourceItem)?.source?.ordinal } buildList() + return true + } + + fun canReorder(oldPos: Int, newPos: Int): Boolean { + val snapshot = items.value?.toMutableList() ?: return false + if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false + if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false + return true } fun setEnabled(source: MangaSource, isEnabled: Boolean) { @@ -49,11 +66,69 @@ class SourcesSettingsViewModel( 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, - ) + val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) { + if (it.name !in hiddenSources) { + KEY_ENABLED + } else { + it.locale + } + } + val result = ArrayList(sources.size + map.size + 1) + val enabledSources = map.remove(KEY_ENABLED) + if (!enabledSources.isNullOrEmpty()) { + result += SourceConfigItem.Header(R.string.enabled_sources) + enabledSources.mapTo(result) { + SourceConfigItem.SourceItem( + source = it, + isEnabled = true, + ) + } + } + if (enabledSources?.size != sources.size) { + result += SourceConfigItem.Header(R.string.available_sources) + for ((key, list) in map) { + val locale = if (key != null) { + Locale(key) + } else null + list.sortBy { it.ordinal } + val isExpanded = key in expandedGroups + result += SourceConfigItem.LocaleGroup( + localeId = key, + title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale), + isExpanded = isExpanded, + ) + if (isExpanded) { + list.mapTo(result) { + SourceConfigItem.SourceItem( + source = it, + isEnabled = false, + ) + } + } + } + } + items.value = result + } + + private class LocaleKeyComparator : Comparator { + + private val deviceLocales = LocaleListCompat.getAdjustedDefault() + .map { it.language } + + override fun compare(a: String?, b: String?): Int { + when { + a == b -> return 0 + a == null -> return 1 + b == null -> return -1 + } + val ai = deviceLocales.indexOf(a!!) + val bi = deviceLocales.indexOf(b!!) + return when { + ai < 0 && bi < 0 -> a.compareTo(b) + ai < 0 -> 1 + bi < 0 -> -1 + else -> ai.compareTo(bi) + } } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt index 7577c89f0..d04d22fcc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt @@ -1,11 +1,13 @@ package org.koitharu.kotatsu.settings.sources.adapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem class SourceConfigAdapter( listener: SourceConfigListener, ) : AsyncListDifferDelegationAdapter( SourceConfigDiffCallback(), - sourceConfigHeaderDelegate(listener), + sourceConfigHeaderDelegate(), + sourceConfigGroupDelegate(listener), sourceConfigItemDelegate(listener), ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt index 16f143a69..df7435bac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt @@ -4,14 +4,27 @@ import android.annotation.SuppressLint import android.view.MotionEvent import android.view.View import android.widget.CompoundButton +import androidx.core.view.isVisible +import androidx.core.view.updatePaddingRelative import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.databinding.ItemExpandableBinding +import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding +import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -fun sourceConfigHeaderDelegate( +fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } +) { + + bind { + binding.root.setText(item.titleResId) + } +} + +fun sourceConfigGroupDelegate( listener: SourceConfigListener, -) = adapterDelegateViewBinding( +) = adapterDelegateViewBinding( { layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) } ) { @@ -57,5 +70,11 @@ fun sourceConfigItemDelegate( bind { binding.textViewTitle.text = item.source.title binding.switchToggle.isChecked = item.isEnabled + binding.imageViewHandle.isVisible = item.isEnabled + binding.imageViewConfig.isVisible = item.isEnabled + binding.root.updatePaddingRelative( + start = if (item.isEnabled) 0 else binding.imageViewHandle.paddingStart * 2, + end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd, + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt index 211ae2395..370cca88d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt @@ -1,32 +1,28 @@ package org.koitharu.kotatsu.settings.sources.adapter import androidx.recyclerview.widget.DiffUtil +import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem class SourceConfigDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean { return when { oldItem.javaClass != newItem.javaClass -> false - oldItem is SourceConfigItem.LocaleHeader && newItem is SourceConfigItem.LocaleHeader -> { + oldItem is SourceConfigItem.LocaleGroup && newItem is SourceConfigItem.LocaleGroup -> { oldItem.localeId == newItem.localeId } oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> { oldItem.source == newItem.source } + oldItem is SourceConfigItem.Header && newItem is SourceConfigItem.Header -> { + oldItem.titleResId == newItem.titleResId + } 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 - } + return oldItem == newItem } override fun getChangePayload(oldItem: SourceConfigItem, newItem: SourceConfigItem) = Unit diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItem.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItem.kt deleted file mode 100644 index b565add2f..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItem.kt +++ /dev/null @@ -1,17 +0,0 @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItemDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItemDecoration.kt new file mode 100644 index 000000000..0171b9dcf --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigItemDecoration.kt @@ -0,0 +1,15 @@ +package org.koitharu.kotatsu.settings.sources.adapter + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.base.ui.list.decor.AbstractDividerItemDecoration + +class SourceConfigItemDecoration(context: Context) : AbstractDividerItemDecoration(context) { + + override fun shouldDrawDivider( + above: RecyclerView.ViewHolder, + below: RecyclerView.ViewHolder, + ): Boolean { + return above.itemViewType != 0 && below.itemViewType != 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt index b34c0e759..8bc03a213 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.settings.sources.adapter import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem interface SourceConfigListener { @@ -10,5 +11,5 @@ interface SourceConfigListener { fun onDragHandleTouch(holder: RecyclerView.ViewHolder) - fun onHeaderClick(header: SourceConfigItem.LocaleHeader) + fun onHeaderClick(header: SourceConfigItem.LocaleGroup) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt new file mode 100644 index 000000000..965ea1171 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt @@ -0,0 +1,72 @@ +package org.koitharu.kotatsu.settings.sources.model + +import androidx.annotation.StringRes +import org.koitharu.kotatsu.core.model.MangaSource + +sealed interface SourceConfigItem { + + class Header( + @StringRes val titleResId: Int, + ) : SourceConfigItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Header + return titleResId == other.titleResId + } + + override fun hashCode(): Int = titleResId + } + + class LocaleGroup( + val localeId: String?, + val title: String?, + val isExpanded: Boolean, + ) : SourceConfigItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LocaleGroup + + if (localeId != other.localeId) return false + if (title != other.title) return false + if (isExpanded != other.isExpanded) return false + + return true + } + + override fun hashCode(): Int { + var result = localeId?.hashCode() ?: 0 + result = 31 * result + (title?.hashCode() ?: 0) + result = 31 * result + isExpanded.hashCode() + return result + } + } + + class SourceItem( + val source: MangaSource, + val isEnabled: Boolean, + ) : SourceConfigItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SourceItem + + if (source != other.source) return false + if (isEnabled != other.isEnabled) return false + + return true + } + + override fun hashCode(): Int { + var result = source.hashCode() + result = 31 * result + isEnabled.hashCode() + return result + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt index 0b1f342d1..3aa96dc2a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext import androidx.collection.ArrayMap import androidx.collection.ArraySet import androidx.collection.LongSparseArray +import java.util.* fun MutableCollection.replaceWith(subject: Iterable) { clear() @@ -72,4 +73,12 @@ fun Collection.isDistinctBy(selector: (T) -> K): Boolean { } } return set.size == size +} + +fun MutableList.move(sourceIndex: Int, targetIndex: Int) { + if (sourceIndex <= targetIndex) { + Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) + } else { + Collections.rotate(subList(targetIndex, sourceIndex + 1), 1) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_source_config.xml b/app/src/main/res/layout/item_source_config.xml index 21a2bb0d9..ffa9a68e5 100644 --- a/app/src/main/res/layout/item_source_config.xml +++ b/app/src/main/res/layout/item_source_config.xml @@ -11,8 +11,8 @@ @@ -36,10 +36,10 @@ diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 34c23028e..cfd85b19d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -246,4 +246,6 @@ Исключить NSFW мангу из истории Имя не может быть пустым Показывать номера страниц + Включенные источники + Доступные источники \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1054d10d..3c63888cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -247,4 +247,6 @@ Exclude NSFW manga from history Name should not be empty Show pages numbers + Enabled sources + Available sources \ No newline at end of file