Search through settings
parent
100073f45e
commit
3d285104a4
@ -0,0 +1,16 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.search
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
data class SettingsItem(
|
||||||
|
val key: String,
|
||||||
|
val title: CharSequence,
|
||||||
|
val breadcrumbs: List<String>,
|
||||||
|
val fragmentClass: Class<out PreferenceFragmentCompat>,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is SettingsItem && other.key == key
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.search
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemPreferenceBinding
|
||||||
|
|
||||||
|
fun settingsItemAD(
|
||||||
|
listener: OnListItemClickListener<SettingsItem>,
|
||||||
|
) = adapterDelegateViewBinding<SettingsItem, SettingsItem, ItemPreferenceBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemPreferenceBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
AdapterDelegateClickListenerAdapter(this, listener).attach()
|
||||||
|
val breadcrumbsSeparator = getString(R.string.breadcrumbs_separator)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = item.title
|
||||||
|
binding.textViewSummary.textAndVisible = item.breadcrumbs.joinToString(breadcrumbsSeparator)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.search
|
||||||
|
|
||||||
|
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.activityViewModels
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class SettingsSearchFragment : BaseFragment<FragmentSearchSuggestionBinding>(), OnListItemClickListener<SettingsItem> {
|
||||||
|
|
||||||
|
private val viewModel: SettingsSearchViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSearchSuggestionBinding {
|
||||||
|
return FragmentSearchSuggestionBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewBindingCreated(binding: FragmentSearchSuggestionBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
|
val adapter = BaseListAdapter<SettingsItem>()
|
||||||
|
.addDelegate(ListItemType.NAV_ITEM, settingsItemAD(this))
|
||||||
|
binding.root.adapter = adapter
|
||||||
|
binding.root.setHasFixedSize(true)
|
||||||
|
viewModel.content.observe(viewLifecycleOwner, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
val extraPadding = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||||
|
requireViewBinding().root.updatePadding(
|
||||||
|
top = extraPadding,
|
||||||
|
right = insets.right,
|
||||||
|
left = insets.left,
|
||||||
|
bottom = insets.bottom,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: SettingsItem, view: View) = viewModel.navigateToPreference(item)
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.search
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.XmlRes
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.get
|
||||||
|
import dagger.Reusable
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.settings.AppearanceSettingsFragment
|
||||||
|
import org.koitharu.kotatsu.settings.DownloadsSettingsFragment
|
||||||
|
import org.koitharu.kotatsu.settings.NetworkSettingsFragment
|
||||||
|
import org.koitharu.kotatsu.settings.ReaderSettingsFragment
|
||||||
|
import org.koitharu.kotatsu.settings.ServicesSettingsFragment
|
||||||
|
import org.koitharu.kotatsu.settings.about.AboutSettingsFragment
|
||||||
|
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||||
|
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
||||||
|
import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
class SettingsSearchHelper @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun inflatePreferences(): List<SettingsItem> {
|
||||||
|
val preferenceManager = PreferenceManager(context)
|
||||||
|
val result = ArrayList<SettingsItem>()
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_appearance, emptyList(), AppearanceSettingsFragment::class.java)
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_sources, emptyList(), SourcesSettingsFragment::class.java)
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_reader, emptyList(), ReaderSettingsFragment::class.java)
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_network, emptyList(), NetworkSettingsFragment::class.java)
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_user_data, emptyList(), UserDataSettingsFragment::class.java)
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_downloads, emptyList(), DownloadsSettingsFragment::class.java)
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_tracker, emptyList(), TrackerSettingsFragment::class.java)
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_services, emptyList(), ServicesSettingsFragment::class.java)
|
||||||
|
preferenceManager.inflateTo(result, R.xml.pref_about, emptyList(), AboutSettingsFragment::class.java)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PreferenceManager.inflateTo(
|
||||||
|
result: MutableList<SettingsItem>,
|
||||||
|
@XmlRes resId: Int,
|
||||||
|
breadcrumbs: List<String>,
|
||||||
|
fragmentClass: Class<out PreferenceFragmentCompat>
|
||||||
|
) {
|
||||||
|
val screen = inflateFromResource(context, resId, null)
|
||||||
|
val screenTitle = screen.title?.toString()
|
||||||
|
screen.inflateTo(
|
||||||
|
result = result,
|
||||||
|
breadcrumbs = if (screenTitle.isNullOrEmpty()) breadcrumbs else breadcrumbs + screenTitle,
|
||||||
|
fragmentClass = fragmentClass,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PreferenceScreen.inflateTo(
|
||||||
|
result: MutableList<SettingsItem>,
|
||||||
|
breadcrumbs: List<String>,
|
||||||
|
fragmentClass: Class<out PreferenceFragmentCompat>
|
||||||
|
): Unit = repeat(preferenceCount) { i ->
|
||||||
|
val pref = this[i]
|
||||||
|
if (pref is PreferenceScreen) {
|
||||||
|
val screenTitle = pref.title?.toString()
|
||||||
|
pref.inflateTo(
|
||||||
|
result = result,
|
||||||
|
breadcrumbs = if (screenTitle.isNullOrEmpty()) breadcrumbs else breadcrumbs + screenTitle,
|
||||||
|
fragmentClass = fragmentClass,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result.add(
|
||||||
|
SettingsItem(
|
||||||
|
key = pref.key ?: return@repeat,
|
||||||
|
title = pref.title ?: return@repeat,
|
||||||
|
breadcrumbs = breadcrumbs,
|
||||||
|
fragmentClass = fragmentClass,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.search
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
|
class SettingsSearchMenuProvider(
|
||||||
|
private val viewModel: SettingsSearchViewModel,
|
||||||
|
) : MenuProvider, MenuItem.OnActionExpandListener, SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.opt_search, menu)
|
||||||
|
val menuItem = menu.findItem(R.id.action_search)
|
||||||
|
menuItem.setOnActionExpandListener(this)
|
||||||
|
val searchView = menuItem.actionView as SearchView
|
||||||
|
searchView.setOnQueryTextListener(this)
|
||||||
|
searchView.queryHint = menuItem.title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareMenu(menu: Menu) {
|
||||||
|
super.onPrepareMenu(menu)
|
||||||
|
val currentQuery = viewModel.currentQuery
|
||||||
|
if (currentQuery.isNotEmpty()) {
|
||||||
|
val menuItem = menu.findItem(R.id.action_search)
|
||||||
|
menuItem.expandActionView()
|
||||||
|
val searchView = menuItem.actionView as SearchView
|
||||||
|
searchView.setQuery(currentQuery, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
|
||||||
|
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean = true
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
|
viewModel.discardSearch()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
viewModel.onQueryChanged(newText.orEmpty())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.search
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SettingsSearchViewModel @Inject constructor(
|
||||||
|
private val searchHelper: SettingsSearchHelper,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val query = MutableStateFlow("")
|
||||||
|
private val allSettings by lazy {
|
||||||
|
searchHelper.inflatePreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
val content = query.map { q ->
|
||||||
|
allSettings.filter { it.title.contains(q, ignoreCase = true) }
|
||||||
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
|
val isSearchActive = query.map {
|
||||||
|
it.isNotEmpty()
|
||||||
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||||
|
|
||||||
|
val onNavigateToPreference = MutableEventFlow<SettingsItem>()
|
||||||
|
val currentQuery: String
|
||||||
|
get() = query.value
|
||||||
|
|
||||||
|
fun onQueryChanged(value: String) {
|
||||||
|
query.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun discardSearch() = onQueryChanged("")
|
||||||
|
|
||||||
|
fun navigateToPreference(item: SettingsItem) {
|
||||||
|
onNavigateToPreference.call(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
|
tools:text="@string/too_many_requests_message" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_summary"
|
||||||
|
style="@style/PreferenceSummaryTextStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
tools:text="@string/tap_to_try_again" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="breadcrumbs_separator" translatable="false"><![CDATA[" < "]]></string>
|
||||||
|
</resources>
|
||||||
Loading…
Reference in New Issue