Quick search across manga sources
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources
|
package org.koitharu.kotatsu.settings.sources
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.*
|
||||||
import android.view.View
|
import androidx.appcompat.widget.SearchView
|
||||||
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.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
@@ -20,7 +19,7 @@ import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
|||||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||||
|
|
||||||
class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
||||||
SourceConfigListener {
|
SourceConfigListener, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
|
||||||
|
|
||||||
private lateinit var reorderHelper: ItemTouchHelper
|
private lateinit var reorderHelper: ItemTouchHelper
|
||||||
private val viewModel by viewModel<SourcesSettingsViewModel>()
|
private val viewModel by viewModel<SourcesSettingsViewModel>()
|
||||||
@@ -60,6 +59,17 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
inflater.inflate(R.menu.opt_sources, menu)
|
||||||
|
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||||
|
searchMenuItem.setOnActionExpandListener(this)
|
||||||
|
val searchView = searchMenuItem.actionView as SearchView
|
||||||
|
searchView.setOnQueryTextListener(this)
|
||||||
|
searchView.setIconifiedByDefault(false)
|
||||||
|
searchView.queryHint = searchMenuItem.title
|
||||||
|
}
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
binding.recyclerView.updatePadding(
|
binding.recyclerView.updatePadding(
|
||||||
bottom = insets.bottom,
|
bottom = insets.bottom,
|
||||||
@@ -84,6 +94,20 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
|
|||||||
viewModel.expandOrCollapse(header.localeId)
|
viewModel.expandOrCollapse(header.localeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
viewModel.performSearch(newText)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
|
(item.actionView as SearchView).setQuery("", false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
|
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
|
||||||
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
|
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class SourcesSettingsViewModel(
|
|||||||
|
|
||||||
val items = MutableLiveData<List<SourceConfigItem>>(emptyList())
|
val items = MutableLiveData<List<SourceConfigItem>>(emptyList())
|
||||||
private val expandedGroups = HashSet<String?>()
|
private val expandedGroups = HashSet<String?>()
|
||||||
|
private var searchQuery: String? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
buildList()
|
buildList()
|
||||||
@@ -63,9 +64,30 @@ class SourcesSettingsViewModel(
|
|||||||
buildList()
|
buildList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun performSearch(query: String?) {
|
||||||
|
searchQuery = query?.trim()
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildList() {
|
private fun buildList() {
|
||||||
val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
|
val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
|
||||||
val hiddenSources = settings.hiddenSources
|
val hiddenSources = settings.hiddenSources
|
||||||
|
val query = searchQuery
|
||||||
|
if (!query.isNullOrEmpty()) {
|
||||||
|
items.value = sources.mapNotNull {
|
||||||
|
if (!it.title.contains(query, ignoreCase = true)) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
SourceConfigItem.SourceItem(
|
||||||
|
source = it,
|
||||||
|
isEnabled = it.name !in hiddenSources,
|
||||||
|
isDraggable = false,
|
||||||
|
)
|
||||||
|
}.ifEmpty {
|
||||||
|
listOf(SourceConfigItem.EmptySearchResult)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
||||||
if (it.name !in hiddenSources) {
|
if (it.name !in hiddenSources) {
|
||||||
KEY_ENABLED
|
KEY_ENABLED
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ class SourceConfigAdapter(
|
|||||||
sourceConfigGroupDelegate(listener),
|
sourceConfigGroupDelegate(listener),
|
||||||
sourceConfigItemDelegate(listener, coil, lifecycleOwner),
|
sourceConfigItemDelegate(listener, coil, lifecycleOwner),
|
||||||
sourceConfigDraggableItemDelegate(listener),
|
sourceConfigDraggableItemDelegate(listener),
|
||||||
|
sourceConfigEmptySearchDelegate(),
|
||||||
)
|
)
|
||||||
@@ -4,12 +4,11 @@ import android.annotation.SuppressLint
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updatePaddingRelative
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.request.Disposable
|
import coil.request.Disposable
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.databinding.ItemExpandableBinding
|
import org.koitharu.kotatsu.databinding.ItemExpandableBinding
|
||||||
@@ -53,25 +52,15 @@ fun sourceConfigItemDelegate(
|
|||||||
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable }
|
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable }
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val eventListener = object : View.OnClickListener, CompoundButton.OnCheckedChangeListener {
|
|
||||||
override fun onClick(v: View?) = listener.onItemSettingsClick(item)
|
|
||||||
|
|
||||||
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
|
|
||||||
listener.onItemEnabledChanged(item, isChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var imageRequest: Disposable? = null
|
var imageRequest: Disposable? = null
|
||||||
|
|
||||||
binding.imageViewConfig.setOnClickListener(eventListener)
|
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
|
||||||
binding.switchToggle.setOnCheckedChangeListener(eventListener)
|
listener.onItemEnabledChanged(item, isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.source.title
|
binding.textViewTitle.text = item.source.title
|
||||||
binding.switchToggle.isChecked = item.isEnabled
|
binding.switchToggle.isChecked = item.isEnabled
|
||||||
binding.imageViewConfig.isVisible = item.isEnabled
|
|
||||||
binding.root.updatePaddingRelative(
|
|
||||||
end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd,
|
|
||||||
)
|
|
||||||
imageRequest = ImageRequest.Builder(context)
|
imageRequest = ImageRequest.Builder(context)
|
||||||
.data(item.faviconUrl)
|
.data(item.faviconUrl)
|
||||||
.error(R.drawable.ic_favicon_fallback)
|
.error(R.drawable.ic_favicon_fallback)
|
||||||
@@ -119,9 +108,9 @@ fun sourceConfigDraggableItemDelegate(
|
|||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.source.title
|
binding.textViewTitle.text = item.source.title
|
||||||
binding.switchToggle.isChecked = item.isEnabled
|
binding.switchToggle.isChecked = item.isEnabled
|
||||||
binding.imageViewConfig.isVisible = item.isEnabled
|
|
||||||
binding.root.updatePaddingRelative(
|
|
||||||
end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sourceConfigEmptySearchDelegate() = adapterDelegate<SourceConfigItem.EmptySearchResult, SourceConfigItem>(
|
||||||
|
R.layout.item_sources_empty
|
||||||
|
) { }
|
||||||
@@ -2,21 +2,25 @@ package org.koitharu.kotatsu.settings.sources.adapter
|
|||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||||
|
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.*
|
||||||
|
|
||||||
class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() {
|
class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() {
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
|
override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
|
||||||
return when {
|
return when {
|
||||||
oldItem.javaClass != newItem.javaClass -> false
|
oldItem.javaClass != newItem.javaClass -> false
|
||||||
oldItem is SourceConfigItem.LocaleGroup && newItem is SourceConfigItem.LocaleGroup -> {
|
oldItem is LocaleGroup && newItem is LocaleGroup -> {
|
||||||
oldItem.localeId == newItem.localeId
|
oldItem.localeId == newItem.localeId
|
||||||
}
|
}
|
||||||
oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> {
|
oldItem is SourceItem && newItem is SourceItem -> {
|
||||||
oldItem.source == newItem.source
|
oldItem.source == newItem.source
|
||||||
}
|
}
|
||||||
oldItem is SourceConfigItem.Header && newItem is SourceConfigItem.Header -> {
|
oldItem is Header && newItem is Header -> {
|
||||||
oldItem.titleResId == newItem.titleResId
|
oldItem.titleResId == newItem.titleResId
|
||||||
}
|
}
|
||||||
|
oldItem == EmptySearchResult && newItem == EmptySearchResult -> {
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,4 +76,6 @@ sealed interface SourceConfigItem {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object EmptySearchResult : SourceConfigItem
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||||
android:background="?android:windowBackground"
|
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@
|
|||||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||||
android:layout_marginHorizontal="?listPreferredItemPaddingStart"
|
android:layout_marginHorizontal="?listPreferredItemPaddingStart"
|
||||||
android:labelFor="@id/textView_title"
|
android:labelFor="@id/textView_title"
|
||||||
android:padding="6dp"
|
android:padding="8dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
tools:src="@tools:sample/avatars" />
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
@@ -33,16 +32,7 @@
|
|||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
android:id="@+id/switch_toggle"
|
android:id="@+id/switch_toggle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="?listPreferredItemPaddingEnd" />
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imageView_config"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/settings"
|
|
||||||
android:paddingHorizontal="?listPreferredItemPaddingEnd"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_settings" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
9
app/src/main/res/layout/item_sources_empty.xml
Normal file
9
app/src/main/res/layout/item_sources_empty.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:text="@string/nothing_found"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:textColor="?android:textColorSecondary" />
|
||||||
@@ -4,9 +4,10 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_languages"
|
android:id="@+id/action_search"
|
||||||
android:icon="@drawable/ic_locale"
|
android:icon="@drawable/ic_search"
|
||||||
android:title="@string/languages"
|
android:title="@string/search"
|
||||||
app:showAsAction="ifRoom" />
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
|
app:showAsAction="ifRoom|collapseActionView" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
Reference in New Issue
Block a user