Search manga with filters
parent
ea5ce23335
commit
e0c983f4eb
@ -0,0 +1,53 @@
|
|||||||
|
package org.koitharu.kotatsu.core.model.parcelable
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parceler
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.TypeParceler
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.readEnumSet
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.writeEnumSet
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Demographic
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
|
||||||
|
object MangaListFilterParceler : Parceler<MangaListFilter> {
|
||||||
|
|
||||||
|
override fun MangaListFilter.write(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(query)
|
||||||
|
parcel.writeParcelable(ParcelableMangaTags(tags), 0)
|
||||||
|
parcel.writeParcelable(ParcelableMangaTags(tagsExclude), 0)
|
||||||
|
parcel.writeSerializable(locale)
|
||||||
|
parcel.writeSerializable(originalLocale)
|
||||||
|
parcel.writeEnumSet(states)
|
||||||
|
parcel.writeEnumSet(contentRating)
|
||||||
|
parcel.writeEnumSet(types)
|
||||||
|
parcel.writeEnumSet(demographics)
|
||||||
|
parcel.writeInt(year)
|
||||||
|
parcel.writeInt(yearFrom)
|
||||||
|
parcel.writeInt(yearTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(parcel: Parcel) = MangaListFilter(
|
||||||
|
query = parcel.readString(),
|
||||||
|
tags = parcel.readParcelableCompat<ParcelableMangaTags>()?.tags.orEmpty(),
|
||||||
|
tagsExclude = parcel.readParcelableCompat<ParcelableMangaTags>()?.tags.orEmpty(),
|
||||||
|
locale = parcel.readSerializableCompat(),
|
||||||
|
originalLocale = parcel.readSerializableCompat(),
|
||||||
|
states = parcel.readEnumSet<MangaState>().orEmpty(),
|
||||||
|
contentRating = parcel.readEnumSet<ContentRating>().orEmpty(),
|
||||||
|
types = parcel.readEnumSet<ContentType>().orEmpty(),
|
||||||
|
demographics = parcel.readEnumSet<Demographic>().orEmpty(),
|
||||||
|
year = parcel.readInt(),
|
||||||
|
yearFrom = parcel.readInt(),
|
||||||
|
yearTo = parcel.readInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@TypeParceler<MangaListFilter, MangaListFilterParceler>
|
||||||
|
data class ParcelableMangaListFilter(val filter: MangaListFilter) : Parcelable
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
package org.koitharu.kotatsu.remotelist.ui
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
|
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
|
||||||
|
class MangaSearchMenuProvider(
|
||||||
|
private val filter: FilterCoordinator,
|
||||||
|
private val viewModel: MangaListViewModel,
|
||||||
|
) : 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)
|
||||||
|
menu.findItem(R.id.action_search)?.isVisible = filter.capabilities.isSearchSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
val snapshot = filter.snapshot()
|
||||||
|
if (!query.isNullOrEmpty() && !filter.capabilities.isSearchWithFiltersSupported && snapshot.listFilter.hasNonSearchOptions()) {
|
||||||
|
filter.set(MangaListFilter(query = query))
|
||||||
|
viewModel.onActionDone.call(
|
||||||
|
ReversibleAction(R.string.filter_search_warning) { filter.set(snapshot.listFilter) },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
filter.setQuery(query)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean = false
|
||||||
|
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
|
(item.actionView as? SearchView)?.run {
|
||||||
|
post { adjustSearchView() }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean = true
|
||||||
|
|
||||||
|
private fun SearchView.adjustSearchView() {
|
||||||
|
imeOptions = if (viewModel.isIncognitoModeEnabled) {
|
||||||
|
imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||||
|
} else {
|
||||||
|
imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
|
||||||
|
}
|
||||||
|
setQuery(filter.query.value, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,97 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.SoftwareKeyboardControllerCompat
|
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.commit
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.core.model.getTitle
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
|
||||||
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
|
|
||||||
|
|
||||||
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
|
|
||||||
private lateinit var source: MangaSource
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(ActivitySearchBinding.inflate(layoutInflater))
|
|
||||||
source = MangaSource(intent.getStringExtra(EXTRA_SOURCE))
|
|
||||||
val query = intent.getStringExtra(EXTRA_QUERY)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
|
|
||||||
with(viewBinding.searchView) {
|
|
||||||
queryHint = getString(R.string.search_on_s, source.getTitle(context))
|
|
||||||
setOnQueryTextListener(this@SearchActivity)
|
|
||||||
|
|
||||||
if (query.isNullOrBlank()) {
|
|
||||||
requestFocus()
|
|
||||||
SoftwareKeyboardControllerCompat(this).show()
|
|
||||||
} else {
|
|
||||||
setQuery(query, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
viewBinding.toolbar.updatePadding(
|
|
||||||
left = insets.left,
|
|
||||||
right = insets.right,
|
|
||||||
top = insets.top,
|
|
||||||
)
|
|
||||||
viewBinding.container.updatePadding(
|
|
||||||
bottom = insets.bottom,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
val q = query?.trim()
|
|
||||||
if (q.isNullOrEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
title = query
|
|
||||||
supportFragmentManager.commit {
|
|
||||||
setReorderingAllowed(true)
|
|
||||||
replace(R.id.container, SearchFragment.newInstance(source, q))
|
|
||||||
}
|
|
||||||
viewBinding.searchView.clearFocus()
|
|
||||||
searchSuggestionViewModel.saveQuery(q)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean = false
|
|
||||||
|
|
||||||
private fun onIncognitoModeChanged(isIncognito: Boolean) {
|
|
||||||
var options = viewBinding.searchView.imeOptions
|
|
||||||
options = if (isIncognito) {
|
|
||||||
options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
|
|
||||||
} else {
|
|
||||||
options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
|
|
||||||
}
|
|
||||||
viewBinding.searchView.imeOptions = options
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val EXTRA_SOURCE = "source"
|
|
||||||
private const val EXTRA_QUERY = "query"
|
|
||||||
|
|
||||||
fun newIntent(context: Context, source: MangaSource, query: String?) =
|
|
||||||
Intent(context, SearchActivity::class.java)
|
|
||||||
.putExtra(EXTRA_SOURCE, source.name)
|
|
||||||
.putExtra(EXTRA_QUERY, query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui
|
|
||||||
|
|
||||||
import android.view.Menu
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class SearchFragment : MangaListFragment() {
|
|
||||||
|
|
||||||
override val viewModel by viewModels<SearchViewModel>()
|
|
||||||
|
|
||||||
override fun onScrolledToEnd() {
|
|
||||||
viewModel.loadNextPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
|
||||||
return super.onCreateActionMode(controller, mode, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val ARG_QUERY = "query"
|
|
||||||
const val ARG_SOURCE = "source"
|
|
||||||
|
|
||||||
fun newInstance(source: MangaSource, query: String) = SearchFragment().withArgs(2) {
|
|
||||||
putString(ARG_SOURCE, source.name)
|
|
||||||
putString(ARG_QUERY, query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.core.model.distinctById
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.require
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
|
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
|
||||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class SearchViewModel @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
repositoryFactory: MangaRepository.Factory,
|
|
||||||
settings: AppSettings,
|
|
||||||
private val mangaListMapper: MangaListMapper,
|
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
|
||||||
) : MangaListViewModel(settings, downloadScheduler) {
|
|
||||||
|
|
||||||
private val query = savedStateHandle.require<String>(SearchFragment.ARG_QUERY)
|
|
||||||
private val repository = repositoryFactory.create(MangaSource(savedStateHandle[SearchFragment.ARG_SOURCE]))
|
|
||||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
|
||||||
private val hasNextPage = MutableStateFlow(false)
|
|
||||||
private val listError = MutableStateFlow<Throwable?>(null)
|
|
||||||
private var loadingJob: Job? = null
|
|
||||||
|
|
||||||
override val content = combine(
|
|
||||||
mangaList.map { it?.skipNsfwIfNeeded() },
|
|
||||||
observeListModeWithTriggers(),
|
|
||||||
listError,
|
|
||||||
hasNextPage,
|
|
||||||
) { list, mode, error, hasNext ->
|
|
||||||
when {
|
|
||||||
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
|
|
||||||
list == null -> listOf(LoadingState)
|
|
||||||
list.isEmpty() -> listOf(
|
|
||||||
EmptyState(
|
|
||||||
icon = R.drawable.ic_empty_common,
|
|
||||||
textPrimary = R.string.nothing_found,
|
|
||||||
textSecondary = R.string.text_search_holder_secondary,
|
|
||||||
actionStringRes = 0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val result = ArrayList<ListModel>(list.size + 1)
|
|
||||||
mangaListMapper.toListModelList(result, list, mode)
|
|
||||||
when {
|
|
||||||
error != null -> result += error.toErrorFooter()
|
|
||||||
hasNext -> result += LoadingFooter()
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadList(append = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
|
||||||
loadList(append = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRetry() {
|
|
||||||
loadList(append = !mangaList.value.isNullOrEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadNextPage() {
|
|
||||||
if (hasNextPage.value && listError.value == null) {
|
|
||||||
loadList(append = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadList(append: Boolean) {
|
|
||||||
if (loadingJob?.isActive == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
|
||||||
try {
|
|
||||||
listError.value = null
|
|
||||||
val list = repository.getList(
|
|
||||||
offset = if (append) mangaList.value.sizeOrZero() else 0,
|
|
||||||
order = null,
|
|
||||||
filter = MangaListFilter(query = query),
|
|
||||||
)
|
|
||||||
val prevList = mangaList.value.orEmpty()
|
|
||||||
if (!append) {
|
|
||||||
mangaList.value = list.distinctById()
|
|
||||||
} else if (list.isNotEmpty()) {
|
|
||||||
mangaList.value = (prevList + list).distinctById()
|
|
||||||
}
|
|
||||||
hasNextPage.value = if (append) {
|
|
||||||
prevList != mangaList.value
|
|
||||||
} else {
|
|
||||||
list.isNotEmpty()
|
|
||||||
}
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
listError.value = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:id="@+id/appbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_scrollFlags="scroll|enterAlways|snap">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.SearchView
|
|
||||||
android:id="@+id/searchView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
app:iconifiedByDefault="false"
|
|
||||||
app:queryBackground="@null"
|
|
||||||
app:searchHintIcon="@null"
|
|
||||||
app:searchIcon="@null" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.MaterialToolbar>
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
|
||||||
android:id="@id/container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_search"
|
||||||
|
android:icon="?actionModeWebSearchDrawable"
|
||||||
|
android:orderInCategory="0"
|
||||||
|
android:title="@string/search"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
|
app:showAsAction="ifRoom|collapseActionView" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
Loading…
Reference in New Issue