Enhance manga search suggestion
This commit is contained in:
@@ -59,6 +59,12 @@ class FilterCoordinator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setTags(tags: Set<MangaTag>) {
|
||||||
|
currentState.update { oldValue ->
|
||||||
|
FilterState(oldValue.sortOrder, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
currentState.update { oldValue ->
|
currentState.update { oldValue ->
|
||||||
FilterState(oldValue.sortOrder, emptySet())
|
FilterState(oldValue.sortOrder, emptySet())
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ class RemoteListViewModel(
|
|||||||
|
|
||||||
fun resetFilter() = filter.reset()
|
fun resetFilter() = filter.reset()
|
||||||
|
|
||||||
|
fun applyFilter(tags: Set<MangaTag>) {
|
||||||
|
filter.setTags(tags)
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadList(filterState: FilterState, append: Boolean) {
|
private fun loadList(filterState: FilterState, append: Boolean) {
|
||||||
if (loadingJob?.isActive == true) {
|
if (loadingJob?.isActive == true) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.model.MangaTag
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
import org.koitharu.kotatsu.databinding.ActivitySearchGlobalBinding
|
import org.koitharu.kotatsu.databinding.ActivitySearchGlobalBinding
|
||||||
import org.koitharu.kotatsu.list.ui.filter.FilterState
|
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ class MangaListActivity : BaseActivity<ActivitySearchGlobalBinding>() {
|
|||||||
val viewModel = fragment.getViewModel<RemoteListViewModel> {
|
val viewModel = fragment.getViewModel<RemoteListViewModel> {
|
||||||
parametersOf(tag.source)
|
parametersOf(tag.source)
|
||||||
}
|
}
|
||||||
viewModel.applyFilter(FilterState(viewModel.filter.sortOrder, setOf(tag)))
|
viewModel.applyFilter(setOf(tag))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.search.ui.suggestion
|
|||||||
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter
|
import org.koitharu.kotatsu.search.ui.suggestion.adapter.SEARCH_SUGGESTION_ITEM_TYPE_QUERY
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
import org.koitharu.kotatsu.utils.ext.getItem
|
import org.koitharu.kotatsu.utils.ext.getItem
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class SearchSuggestionItemCallback(
|
|||||||
override fun getMovementFlags(
|
override fun getMovementFlags(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
): Int = if (viewHolder.itemViewType == SearchSuggestionAdapter.ITEM_TYPE_QUERY) {
|
): Int = if (viewHolder.itemViewType == SEARCH_SUGGESTION_ITEM_TYPE_QUERY) {
|
||||||
movementFlags
|
movementFlags
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package org.koitharu.kotatsu.search.ui.suggestion
|
|||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
@@ -14,11 +12,9 @@ import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
|||||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
|
||||||
private const val DEBOUNCE_TIMEOUT = 500L
|
private const val DEBOUNCE_TIMEOUT = 500L
|
||||||
private const val SEARCH_THRESHOLD = 3
|
private const val MAX_MANGA_ITEMS = 6
|
||||||
private const val MAX_MANGA_ITEMS = 3
|
|
||||||
private const val MAX_QUERY_ITEMS = 16
|
private const val MAX_QUERY_ITEMS = 16
|
||||||
private const val MAX_TAGS_ITEMS = 8
|
private const val MAX_TAGS_ITEMS = 8
|
||||||
private const val MAX_SUGGESTION_ITEMS = MAX_MANGA_ITEMS + MAX_QUERY_ITEMS + 2
|
|
||||||
|
|
||||||
class SearchSuggestionViewModel(
|
class SearchSuggestionViewModel(
|
||||||
private val repository: MangaSearchRepository,
|
private val repository: MangaSearchRepository,
|
||||||
@@ -68,33 +64,49 @@ class SearchSuggestionViewModel(
|
|||||||
private fun setupSuggestion() {
|
private fun setupSuggestion() {
|
||||||
suggestionJob?.cancel()
|
suggestionJob?.cancel()
|
||||||
suggestionJob = combine(
|
suggestionJob = combine(
|
||||||
query
|
query.debounce(DEBOUNCE_TIMEOUT),
|
||||||
.debounce(DEBOUNCE_TIMEOUT)
|
|
||||||
.mapLatest { q ->
|
|
||||||
q to repository.getQuerySuggestion(q, MAX_QUERY_ITEMS)
|
|
||||||
},
|
|
||||||
source,
|
source,
|
||||||
isLocalSearch
|
isLocalSearch,
|
||||||
) { (q, queries), src, srcOnly ->
|
::Triple,
|
||||||
val result = ArrayList<SearchSuggestionItem>(MAX_SUGGESTION_ITEMS)
|
).mapLatest { (searchQuery, src, srcOnly) ->
|
||||||
|
buildSearchSuggestion(searchQuery, src, srcOnly)
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
.onEach {
|
||||||
|
suggestion.postValue(it)
|
||||||
|
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildSearchSuggestion(
|
||||||
|
searchQuery: String,
|
||||||
|
src: MangaSource?,
|
||||||
|
srcOnly: Boolean,
|
||||||
|
): List<SearchSuggestionItem> = coroutineScope {
|
||||||
|
val queriesDeferred = async {
|
||||||
|
repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS)
|
||||||
|
}
|
||||||
|
val tagsDeferred = async {
|
||||||
|
repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, src.takeIf { srcOnly })
|
||||||
|
}
|
||||||
|
val mangaDeferred = async {
|
||||||
|
repository.getMangaSuggestion(searchQuery, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
|
||||||
|
}
|
||||||
|
|
||||||
|
val tags = tagsDeferred.await()
|
||||||
|
val mangaList = mangaDeferred.await()
|
||||||
|
val queries = queriesDeferred.await()
|
||||||
|
|
||||||
|
buildList(queries.size + 3) {
|
||||||
if (src != null) {
|
if (src != null) {
|
||||||
result += SearchSuggestionItem.Header(src, isLocalSearch)
|
add(SearchSuggestionItem.Header(src, isLocalSearch))
|
||||||
}
|
}
|
||||||
val tags = repository.getTagsSuggestion(q, MAX_TAGS_ITEMS, src.takeIf { srcOnly })
|
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
result.add(SearchSuggestionItem.Tags(mapTags(tags)))
|
add(SearchSuggestionItem.Tags(mapTags(tags)))
|
||||||
}
|
}
|
||||||
if (q.length >= SEARCH_THRESHOLD) {
|
if (mangaList.isNotEmpty()) {
|
||||||
repository.getMangaSuggestion(q, MAX_MANGA_ITEMS, src.takeIf { srcOnly })
|
add(SearchSuggestionItem.MangaList(mangaList))
|
||||||
.mapTo(result) {
|
|
||||||
SearchSuggestionItem.MangaItem(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
queries.mapTo(result) { SearchSuggestionItem.RecentQuery(it) }
|
queries.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||||
result
|
}
|
||||||
}.onEach {
|
|
||||||
suggestion.postValue(it)
|
|
||||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
|
private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
|||||||
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
|
const val SEARCH_SUGGESTION_ITEM_TYPE_QUERY = 0
|
||||||
|
|
||||||
class SearchSuggestionAdapter(
|
class SearchSuggestionAdapter(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
@@ -15,10 +17,11 @@ class SearchSuggestionAdapter(
|
|||||||
) : AsyncListDifferDelegationAdapter<SearchSuggestionItem>(DiffCallback()) {
|
) : AsyncListDifferDelegationAdapter<SearchSuggestionItem>(DiffCallback()) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
delegatesManager.addDelegate(ITEM_TYPE_MANGA, searchSuggestionMangaAD(coil, lifecycleOwner, listener))
|
delegatesManager
|
||||||
.addDelegate(ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
.addDelegate(SEARCH_SUGGESTION_ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
|
||||||
.addDelegate(ITEM_TYPE_HEADER, searchSuggestionHeaderAD(listener))
|
.addDelegate(searchSuggestionHeaderAD(listener))
|
||||||
.addDelegate(ITEM_TYPE_TAGS, searchSuggestionTagsAD(listener))
|
.addDelegate(searchSuggestionTagsAD(listener))
|
||||||
|
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<SearchSuggestionItem>() {
|
private class DiffCallback : DiffUtil.ItemCallback<SearchSuggestionItem>() {
|
||||||
@@ -27,15 +30,10 @@ class SearchSuggestionAdapter(
|
|||||||
oldItem: SearchSuggestionItem,
|
oldItem: SearchSuggestionItem,
|
||||||
newItem: SearchSuggestionItem,
|
newItem: SearchSuggestionItem,
|
||||||
): Boolean = when {
|
): 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 is SearchSuggestionItem.RecentQuery && newItem is SearchSuggestionItem.RecentQuery -> {
|
||||||
oldItem.query == newItem.query
|
oldItem.query == newItem.query
|
||||||
}
|
}
|
||||||
oldItem is SearchSuggestionItem.Header && newItem is SearchSuggestionItem.Header -> true
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
oldItem is SearchSuggestionItem.Tags && newItem is SearchSuggestionItem.Tags -> true
|
|
||||||
else -> false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
override fun areContentsTheSame(
|
||||||
@@ -43,12 +41,4 @@ class SearchSuggestionAdapter(
|
|||||||
newItem: SearchSuggestionItem,
|
newItem: SearchSuggestionItem,
|
||||||
): Boolean = Intrinsics.areEqual(oldItem, newItem)
|
): 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
|
|
||||||
const val ITEM_TYPE_TAGS = 3
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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,89 @@
|
|||||||
|
package org.koitharu.kotatsu.search.ui.suggestion.adapter
|
||||||
|
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.request.Disposable
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionMangaGridBinding
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||||
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
import org.koitharu.kotatsu.utils.ScrollResetCallback
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
|
||||||
|
fun searchSuggestionMangaListAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
) = adapterDelegate<SearchSuggestionItem.MangaList, SearchSuggestionItem>(R.layout.item_search_suggestion_manga_list) {
|
||||||
|
|
||||||
|
val adapter = AsyncListDifferDelegationAdapter(
|
||||||
|
SuggestionMangaDiffCallback(),
|
||||||
|
searchSuggestionMangaGridAD(coil, lifecycleOwner, listener),
|
||||||
|
)
|
||||||
|
val recyclerView = itemView as RecyclerView
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
val spacing = context.resources.getDimensionPixelOffset(R.dimen.search_suggestions_manga_spacing)
|
||||||
|
recyclerView.updatePadding(
|
||||||
|
left = recyclerView.paddingLeft - spacing,
|
||||||
|
right = recyclerView.paddingRight - spacing,
|
||||||
|
)
|
||||||
|
recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
||||||
|
val scrollResetCallback = ScrollResetCallback(recyclerView)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
adapter.setItems(item.items, scrollResetCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchSuggestionMangaGridAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
listener: SearchSuggestionListener,
|
||||||
|
) = adapterDelegateViewBinding<Manga, Manga, ItemSearchSuggestionMangaGridBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemSearchSuggestionMangaGridBinding.inflate(layoutInflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
var imageRequest: Disposable? = null
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
listener.onMangaClick(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
imageRequest?.dispose()
|
||||||
|
imageRequest = binding.imageViewCover.newImageRequest(item.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.title
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewRecycled {
|
||||||
|
imageRequest?.dispose()
|
||||||
|
binding.imageViewCover.setImageDrawable(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SuggestionMangaDiffCallback : DiffUtil.ItemCallback<Manga>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: Manga, newItem: Manga): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Manga, newItem: Manga): Boolean {
|
||||||
|
return oldItem.title == newItem.title && oldItem.coverUrl == newItem.coverUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,23 +4,95 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.utils.ext.areItemsEquals
|
||||||
|
|
||||||
sealed interface SearchSuggestionItem {
|
sealed interface SearchSuggestionItem {
|
||||||
|
|
||||||
data class MangaItem(
|
class MangaList(
|
||||||
val manga: Manga,
|
val items: List<Manga>,
|
||||||
) : SearchSuggestionItem
|
) : SearchSuggestionItem {
|
||||||
|
|
||||||
data class RecentQuery(
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as MangaList
|
||||||
|
|
||||||
|
return items.areItemsEquals(other.items) { a, b ->
|
||||||
|
a.title == b.title && a.coverUrl == b.coverUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return items.fold(0) { acc, t ->
|
||||||
|
var r = 31 * acc + t.title.hashCode()
|
||||||
|
r = 31 * r + t.coverUrl.hashCode()
|
||||||
|
r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecentQuery(
|
||||||
val query: String,
|
val query: String,
|
||||||
) : SearchSuggestionItem
|
) : SearchSuggestionItem {
|
||||||
|
|
||||||
data class Header(
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as RecentQuery
|
||||||
|
|
||||||
|
if (query != other.query) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return query.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Header(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
val isChecked: MutableStateFlow<Boolean>,
|
val isChecked: MutableStateFlow<Boolean>,
|
||||||
) : SearchSuggestionItem
|
) : SearchSuggestionItem {
|
||||||
|
|
||||||
data class Tags(
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Header
|
||||||
|
|
||||||
|
if (source != other.source) return false
|
||||||
|
if (isChecked !== other.isChecked) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = source.hashCode()
|
||||||
|
result = 31 * result + isChecked.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tags(
|
||||||
val tags: List<ChipsView.ChipModel>,
|
val tags: List<ChipsView.ChipModel>,
|
||||||
) : SearchSuggestionItem
|
) : SearchSuggestionItem {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Tags
|
||||||
|
|
||||||
|
if (tags != other.tags) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return tags.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
class ScrollResetCallback(recyclerView: RecyclerView) : Runnable {
|
||||||
|
|
||||||
|
private val recyclerViewRef = WeakReference(recyclerView)
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
recyclerViewRef.get()?.scrollToPosition(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
import android.util.SparseArray
|
|
||||||
import androidx.collection.ArrayMap
|
import androidx.collection.ArrayMap
|
||||||
import androidx.collection.ArraySet
|
import androidx.collection.ArraySet
|
||||||
import androidx.collection.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
@@ -82,4 +81,18 @@ fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
|
|||||||
} else {
|
} else {
|
||||||
Collections.rotate(subList(targetIndex, sourceIndex + 1), 1)
|
Collections.rotate(subList(targetIndex, sourceIndex + 1), 1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> List<T>.areItemsEquals(other: List<T>, equals: (T, T) -> Boolean): Boolean {
|
||||||
|
if (size != other.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (i in indices) {
|
||||||
|
val a = this[i]
|
||||||
|
val b = other[i]
|
||||||
|
if (!equals(a, b)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
|
android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar"
|
||||||
app:contentInsetStartWithNavigation="0dp"
|
app:contentInsetStartWithNavigation="0dp"
|
||||||
app:titleTextAppearance="@style/TextAppearance.Kotatsu.PersistentToolbarTitle"
|
app:titleTextAppearance="@style/TextAppearance.Kotatsu.PersistentToolbarTitle"
|
||||||
app:titleTextColor="?android:colorControlNormal"
|
app:titleTextColor="?android:colorControlNormal"
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
style="@style/Widget.Kotatsu.SearchView"
|
style="@style/Widget.Kotatsu.SearchView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="2dp"
|
||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:hint="@string/search_manga"
|
android:hint="@string/search_manga"
|
||||||
@@ -59,7 +61,7 @@
|
|||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:paddingBottom="1dp"
|
android:paddingBottom="1dp"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
tools:drawableEnd="@drawable/ic_clear" />
|
tools:drawableEnd="@drawable/abc_ic_clear_material" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.MaterialToolbar>
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
<?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="?attr/textAppearanceBodyMedium"
|
|
||||||
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="?attr/textAppearanceBodySmall"
|
|
||||||
android:textColor="?android:textColorSecondary"
|
|
||||||
tools:text="@tools:sample/lorem[6]" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
style="@style/Widget.Material3.CardView.Outlined"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:contentPadding="4dp"
|
||||||
|
tools:layout_height="@dimen/search_suggestions_manga_height">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
|
||||||
|
android:id="@+id/imageView_cover"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:elegantTextHeight="false"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
tools:text="@tools:sample/lorem[6]" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:scrollbars="none"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?listPreferredItemPaddingEnd"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_search_suggestion_manga_grid"
|
||||||
|
android:layout_height="@dimen/search_suggestions_manga_height" />
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
<dimen name="list_footer_height_outer">48dp</dimen>
|
<dimen name="list_footer_height_outer">48dp</dimen>
|
||||||
<dimen name="screen_padding">16dp</dimen>
|
<dimen name="screen_padding">16dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="search_suggestions_manga_height">124dp</dimen>
|
||||||
|
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
|
||||||
<!--Text dimens-->
|
<!--Text dimens-->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources>
|
||||||
|
|
||||||
<!--Toolbars-->
|
<!--Toolbars-->
|
||||||
|
|
||||||
@@ -76,6 +76,10 @@
|
|||||||
<item name="elevationOverlayEnabled">@bool/elevation_overlay_enabled</item>
|
<item name="elevationOverlayEnabled">@bool/elevation_overlay_enabled</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="ThemeOverlay.Kotatsu.MainToolbar" parent="">
|
||||||
|
<item name="colorControlHighlight">@color/selector_overlay</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- TextAppearance -->
|
<!-- TextAppearance -->
|
||||||
|
|
||||||
<style name="TextAppearance.Widget.Menu" parent="TextAppearance.AppCompat.Menu">
|
<style name="TextAppearance.Widget.Menu" parent="TextAppearance.AppCompat.Menu">
|
||||||
@@ -100,6 +104,10 @@
|
|||||||
<item name="cornerSize">12dp</item>
|
<item name="cornerSize">12dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="ShapeAppearanceOverlay.Kotatsu.Cover.Small" parent="">
|
||||||
|
<item name="cornerSize">6dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!--Preferences-->
|
<!--Preferences-->
|
||||||
|
|
||||||
<style name="PreferenceThemeOverlay.Kotatsu">
|
<style name="PreferenceThemeOverlay.Kotatsu">
|
||||||
|
|||||||
Reference in New Issue
Block a user