New search suggestion UI
parent
bc0c5ac71a
commit
cd7d6d7674
@ -1,53 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui
|
|
||||||
|
|
||||||
import android.app.SearchManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
|
|
||||||
import java.io.Closeable
|
|
||||||
|
|
||||||
object SearchHelper {
|
|
||||||
|
|
||||||
fun setupSearchView(menuItem: MenuItem): Closeable? {
|
|
||||||
val view = menuItem.actionView as? SearchView ?: return null
|
|
||||||
val context = view.context
|
|
||||||
val adapter = MangaSuggestionsProvider.getSuggestionAdapter(context)
|
|
||||||
view.queryHint = context.getString(R.string.search_manga)
|
|
||||||
view.suggestionsAdapter = adapter
|
|
||||||
view.setOnQueryTextListener(QueryListener(context))
|
|
||||||
view.setOnSuggestionListener(SuggestionListener(view))
|
|
||||||
return adapter?.cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
private class QueryListener(private val context: Context) :
|
|
||||||
SearchView.OnQueryTextListener {
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
return if (!query.isNullOrBlank()) {
|
|
||||||
context.startActivity(GlobalSearchActivity.newIntent(context, query.trim()))
|
|
||||||
MangaSuggestionsProvider.saveQueryAsync(context.applicationContext, query)
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?) = false
|
|
||||||
}
|
|
||||||
|
|
||||||
class SuggestionListener(private val view: SearchView) :
|
|
||||||
SearchView.OnSuggestionListener {
|
|
||||||
|
|
||||||
override fun onSuggestionSelect(position: Int) = false
|
|
||||||
|
|
||||||
override fun onSuggestionClick(position: Int): Boolean {
|
|
||||||
val query = runCatching {
|
|
||||||
val c = view.suggestionsAdapter.getItem(position) as? Cursor
|
|
||||||
c?.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY))
|
|
||||||
}.getOrNull() ?: return false
|
|
||||||
view.setQuery(query, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion
|
||||||
|
|
||||||
|
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.recyclerview.widget.ItemTouchHelper
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
|
import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter
|
||||||
|
|
||||||
|
class SearchSuggestionFragment : BaseFragment<FragmentSearchSuggestionBinding>(),
|
||||||
|
SearchSuggestionItemCallback.SuggestionItemListener {
|
||||||
|
|
||||||
|
private val viewModel by sharedViewModel<SearchSuggestionViewModel>()
|
||||||
|
|
||||||
|
override fun onInflateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
) = FragmentSearchSuggestionBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val adapter = SearchSuggestionAdapter(
|
||||||
|
coil = get(),
|
||||||
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
|
listener = requireActivity() as SearchSuggestionListener,
|
||||||
|
)
|
||||||
|
binding.root.adapter = adapter
|
||||||
|
viewModel.suggestion.observe(viewLifecycleOwner) {
|
||||||
|
adapter.items = it
|
||||||
|
}
|
||||||
|
ItemTouchHelper(SearchSuggestionItemCallback(this))
|
||||||
|
.attachToRecyclerView(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
binding.root.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
bottom = insets.bottom,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoveQuery(query: String) {
|
||||||
|
viewModel.deleteQuery(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance() = SearchSuggestionFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getItem
|
||||||
|
|
||||||
|
class SearchSuggestionItemCallback(
|
||||||
|
private val listener: SuggestionItemListener,
|
||||||
|
) : ItemTouchHelper.Callback() {
|
||||||
|
|
||||||
|
private val movementFlags = makeMovementFlags(
|
||||||
|
0,
|
||||||
|
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun getMovementFlags(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
): Int = if (viewHolder.itemViewType == SearchSuggestionAdapter.ITEM_TYPE_QUERY) {
|
||||||
|
movementFlags
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder,
|
||||||
|
): Boolean = false
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
val item = viewHolder.getItem<SearchSuggestionItem.RecentQuery>() ?: return
|
||||||
|
listener.onRemoveQuery(item.query)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SuggestionItemListener {
|
||||||
|
|
||||||
|
fun onRemoveQuery(query: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
|
||||||
|
interface SearchSuggestionListener {
|
||||||
|
|
||||||
|
fun onMangaClick(manga: Manga)
|
||||||
|
|
||||||
|
fun onQueryClick(query: String, submit: Boolean)
|
||||||
|
|
||||||
|
fun onQueryChanged(query: String)
|
||||||
|
|
||||||
|
fun onClearSearchHistory()
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
|
||||||
|
class SearchSuggestionViewModel(
|
||||||
|
private val repository: MangaSearchRepository,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val query = MutableStateFlow("")
|
||||||
|
private val source = MutableStateFlow<MangaSource?>(null)
|
||||||
|
private val isLocalSearch = MutableStateFlow(false)
|
||||||
|
private var suggestionJob: Job? = null
|
||||||
|
|
||||||
|
val suggestion = MutableLiveData<List<SearchSuggestionItem>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
setupSuggestion()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onQueryChanged(newQuery: String) {
|
||||||
|
query.value = newQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSourceChanged(newSource: MangaSource?) {
|
||||||
|
source.value = newSource
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveQuery(query: String) {
|
||||||
|
repository.saveSearchQuery(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocalSearchSource(): MangaSource? {
|
||||||
|
return source.value?.takeIf { isLocalSearch.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSearchHistory() {
|
||||||
|
launchJob {
|
||||||
|
repository.clearSearchHistory()
|
||||||
|
setupSuggestion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteQuery(query: String) {
|
||||||
|
launchJob {
|
||||||
|
repository.deleteSearchQuery(query)
|
||||||
|
setupSuggestion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSuggestion() {
|
||||||
|
suggestionJob?.cancel()
|
||||||
|
suggestionJob = combine(
|
||||||
|
query
|
||||||
|
.debounce(DEBOUNCE_TIMEOUT)
|
||||||
|
.mapLatest { q ->
|
||||||
|
q to repository.getQuerySuggestion(q, MAX_QUERY_ITEMS)
|
||||||
|
},
|
||||||
|
source,
|
||||||
|
isLocalSearch
|
||||||
|
) { (q, queries), src, srcOnly ->
|
||||||
|
val result = ArrayList<SearchSuggestionItem>(MAX_SUGGESTION_ITEMS)
|
||||||
|
if (src != null) {
|
||||||
|
result += SearchSuggestionItem.Header(src, isLocalSearch)
|
||||||
|
}
|
||||||
|
if (q.length >= SEARCH_THRESHOLD) {
|
||||||
|
repository.getMangaSuggestion(q, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
|
||||||
|
.mapTo(result) {
|
||||||
|
SearchSuggestionItem.MangaItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queries.mapTo(result) { SearchSuggestionItem.RecentQuery(it) }
|
||||||
|
result
|
||||||
|
}.onEach {
|
||||||
|
suggestion.postValue(it)
|
||||||
|
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val DEBOUNCE_TIMEOUT = 500L
|
||||||
|
const val SEARCH_THRESHOLD = 3
|
||||||
|
const val MAX_MANGA_ITEMS = 3
|
||||||
|
const val MAX_QUERY_ITEMS = 120
|
||||||
|
const val MAX_SUGGESTION_ITEMS = MAX_MANGA_ITEMS + MAX_QUERY_ITEMS + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion
|
||||||
|
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
|
class SearchUI(
|
||||||
|
private val searchView: SearchView,
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
hint: String? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
val context = searchView.context
|
||||||
|
searchView.queryHint = hint ?: context.getString(R.string.search_manga)
|
||||||
|
searchView.setOnQueryTextListener(QueryListener(listener))
|
||||||
|
}
|
||||||
|
|
||||||
|
var query: String
|
||||||
|
get() = searchView.query.toString()
|
||||||
|
set(value) {
|
||||||
|
searchView.setQuery(value, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QueryListener(
|
||||||
|
private val listener: SearchSuggestionListener,
|
||||||
|
) : SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
return if (!query.isNullOrBlank()) {
|
||||||
|
listener.onQueryClick(query.trim(), submit = true)
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
listener.onQueryChanged(newText?.trim().orEmpty())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun from(
|
||||||
|
menuItem: MenuItem,
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
): SearchUI? = (menuItem.actionView as? SearchView)?.let {
|
||||||
|
SearchUI(it, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
|
class SearchSuggestionAdapter(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
) : AsyncListDifferDelegationAdapter<SearchSuggestionItem>(DiffCallback()) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegatesManager.addDelegate(ITEM_TYPE_MANGA, searchSuggestionMangaAD(coil, lifecycleOwner, listener))
|
||||||
|
.addDelegate(ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
||||||
|
.addDelegate(ITEM_TYPE_HEADER, searchSuggestionHeaderAD(listener))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<SearchSuggestionItem>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: SearchSuggestionItem,
|
||||||
|
newItem: SearchSuggestionItem,
|
||||||
|
): Boolean = when {
|
||||||
|
oldItem is SearchSuggestionItem.MangaItem && newItem is SearchSuggestionItem.MangaItem -> {
|
||||||
|
oldItem.manga.id == newItem.manga.id
|
||||||
|
}
|
||||||
|
oldItem is SearchSuggestionItem.RecentQuery && newItem is SearchSuggestionItem.RecentQuery -> {
|
||||||
|
oldItem.query == newItem.query
|
||||||
|
}
|
||||||
|
oldItem is SearchSuggestionItem.Header && newItem is SearchSuggestionItem.Header -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: SearchSuggestionItem,
|
||||||
|
newItem: SearchSuggestionItem,
|
||||||
|
): Boolean = Intrinsics.areEqual(oldItem, newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val ITEM_TYPE_MANGA = 0
|
||||||
|
const val ITEM_TYPE_QUERY = 1
|
||||||
|
const val ITEM_TYPE_HEADER = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionHeaderBinding
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
|
||||||
|
fun searchSuggestionHeaderAD(
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
) = adapterDelegateViewBinding<SearchSuggestionItem.Header, SearchSuggestionItem, ItemSearchSuggestionHeaderBinding>(
|
||||||
|
{ inflater, parent -> ItemSearchSuggestionHeaderBinding.inflate(inflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.switchLocal.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
item.isChecked.value = isChecked
|
||||||
|
}
|
||||||
|
binding.buttonClear.setOnClickListener {
|
||||||
|
listener.onClearSearchHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.switchLocal.text = getString(
|
||||||
|
R.string.search_only_on_s,
|
||||||
|
item.source.title,
|
||||||
|
)
|
||||||
|
binding.switchLocal.isChecked = item.isChecked.value
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.request.Disposable
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionMangaBinding
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
|
|
||||||
|
fun searchSuggestionMangaAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
) = adapterDelegateViewBinding<SearchSuggestionItem.MangaItem, SearchSuggestionItem, ItemSearchSuggestionMangaBinding>(
|
||||||
|
{ inflater, parent -> ItemSearchSuggestionMangaBinding.inflate(inflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
var imageRequest: Disposable? = null
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
listener.onMangaClick(item.manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
imageRequest?.dispose()
|
||||||
|
imageRequest = binding.imageViewCover.newImageRequest(item.manga.coverUrl)
|
||||||
|
.placeholder(R.drawable.ic_placeholder)
|
||||||
|
.fallback(R.drawable.ic_placeholder)
|
||||||
|
.error(R.drawable.ic_placeholder)
|
||||||
|
.allowRgb565(true)
|
||||||
|
.lifecycle(lifecycleOwner)
|
||||||
|
.enqueueWith(coil)
|
||||||
|
binding.textViewTitle.text = item.manga.title
|
||||||
|
binding.textViewSubtitle.textAndVisible = item.manga.altTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewRecycled {
|
||||||
|
imageRequest?.dispose()
|
||||||
|
binding.imageViewCover.setImageDrawable(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionQueryBinding
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
|
||||||
|
fun searchSuggestionQueryAD(
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
) = adapterDelegateViewBinding<SearchSuggestionItem.RecentQuery, SearchSuggestionItem, ItemSearchSuggestionQueryBinding>(
|
||||||
|
{ inflater, parent -> ItemSearchSuggestionQueryBinding.inflate(inflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
val viewClickListener = View.OnClickListener { v ->
|
||||||
|
listener.onQueryClick(item.query, v.id != R.id.button_complete)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root.setOnClickListener(viewClickListener)
|
||||||
|
binding.buttonComplete.setOnClickListener(viewClickListener)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = item.query
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion.model
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
|
||||||
|
sealed class SearchSuggestionItem {
|
||||||
|
|
||||||
|
data class MangaItem(
|
||||||
|
val manga: Manga,
|
||||||
|
) : SearchSuggestionItem()
|
||||||
|
|
||||||
|
data class RecentQuery(
|
||||||
|
val query: String,
|
||||||
|
) : SearchSuggestionItem()
|
||||||
|
|
||||||
|
data class Header(
|
||||||
|
val source: MangaSource,
|
||||||
|
val isChecked: MutableStateFlow<Boolean>,
|
||||||
|
) : SearchSuggestionItem()
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M5,13h14v-2L5,11v2zM3,17h14v-2L3,15v2zM7,7v2h14L21,7L7,7z" />
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,17.59L17.59,19L7,8.41V15H5V5H15V7H8.41L19,17.59Z" />
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
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="match_parent"
|
||||||
|
android:background="?android:windowBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:ignore="KeyboardInaccessibleWidget" />
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?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:gravity="center_vertical"
|
||||||
|
android:minHeight="?listPreferredItemHeightSmall"
|
||||||
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?listPreferredItemPaddingEnd">
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_local"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="?listPreferredItemPaddingEnd"
|
||||||
|
android:layout_weight="1"
|
||||||
|
tools:text="@string/search_only_on_s" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_clear"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_clear_all" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
<?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:gravity="center_vertical"
|
||||||
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||||
|
android:paddingBottom="2dp">
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
|
||||||
|
android:id="@+id/imageView_cover"
|
||||||
|
android:layout_width="42dp"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
tools:text="@tools:sample/lorem[6]" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
tools:text="@tools:sample/lorem[6]" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
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="wrap_content"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?listPreferredItemHeightSmall"
|
||||||
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?listPreferredItemPaddingEnd">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:drawablePadding="12dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
app:drawableStartCompat="@drawable/ic_history"
|
||||||
|
tools:text="@tools:sample/lorem[6]" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_complete"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/ic_complete" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
Loading…
Reference in New Issue