feat(filter): revamping search for anime and manga (#272)

* feat: revamping search filter part1

* fix: sortBy dropdown now also calls search instead of only updating image

* feat: added longclick listener to reset and apply + cleaned up code

* feat: status filter fully functional

* chore: upgrade AGP to 8.3.1

* fix: splitted status list and cleaned up

* fix(search): underscore

* feat: attempt to add backend for countryOfOrigin filter

* fix: countryOfOrigin query and gradle

* feat: source filter fully functional

* fix(source): underscore

* feat: swap source with status

* fix: add searchSource to reset fun

* fix: clear underline after reopening bottom sheet

* chore: remove unnecessary declaration

* feat: add global to countryOfOrigin dropdown

* feat: floating cancel and apply button

* fix: added searchStatus and searchYear back to manga filter

* feat: desperate attempt for manga year filter

* feat(sortBy): added new releases item

* fix: year filter

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
This commit is contained in:
ibo 2024-04-02 05:13:41 +02:00 committed by GitHub
parent aabbe9198a
commit 146805af49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 971 additions and 333 deletions

View file

@ -74,8 +74,12 @@ class SearchActivity : AppCompatActivity() {
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"),
status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
results = mutableListOf(),
hasNextPage = false
)
@ -134,8 +138,12 @@ class SearchActivity : AppCompatActivity() {
excludedTags = it.excludedTags
tags = it.tags
season = it.season
startYear = it.startYear
seasonYear = it.seasonYear
status = it.status
source = it.source
format = it.format
countryOfOrigin = it.countryOfOrigin
page = it.page
hasNextPage = it.hasNextPage
}

View file

@ -13,6 +13,8 @@ import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
@ -45,6 +47,19 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@ -93,7 +108,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
activity.updateChips = { it.update() }
}
@ -103,6 +118,59 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
}
binding.searchFilter.setOnLongClickListener {
val popupMenu = PopupMenu(activity, binding.searchFilter)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
}
true
}
popupMenu.show()
true
}
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
@ -257,7 +325,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
}
class SearchChipAdapter(val activity: SearchActivity) :
class SearchChipAdapter(val activity: SearchActivity, private val searchAdapter: SearchAdapter) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()
@ -274,11 +342,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val chip = chips[position]
holder.binding.root.apply {
text = chip.text
text = chip.text.replace("_", " ")
setOnClickListener {
activity.result.removeChip(chip)
update()
activity.search()
searchAdapter.updateFilterTextViewDrawable()
}
}
}
@ -287,6 +356,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
fun update() {
chips = activity.result.toChipList()
notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
}
override fun getItemCount(): Int = chips.size

View file

@ -1,11 +1,15 @@
package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AnimationUtils
import android.widget.ArrayAdapter
import android.widget.PopupMenu
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -17,6 +21,9 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
import ani.dantotsu.databinding.ItemChipBinding
import com.google.android.material.chip.Chip
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Calendar
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
@ -38,6 +45,54 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
private var exGenres = mutableListOf<String>()
private var selectedTags = mutableListOf<String>()
private var exTags = mutableListOf<String>()
private fun updateChips() {
binding.searchFilterGenres.adapter?.notifyDataSetChanged()
binding.searchFilterTags.adapter?.notifyDataSetChanged()
}
private fun startBounceZoomAnimation(view: View? = null) {
val targetView = view ?: binding.sortByFilter
val bounceZoomAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
targetView.startAnimation(bounceZoomAnimation)
}
private fun setSortByFilterImage() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.sortByFilter.setImageResource(filterDrawable)
}
private fun resetSearchFilter() {
activity.result.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter)
activity.result.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
selectedGenres.clear()
exGenres.clear()
selectedTags.clear()
exTags.clear()
binding.searchStatus.setText("")
binding.searchSource.setText("")
binding.searchFormat.setText("")
binding.searchSeason.setText("")
binding.searchYear.setText("")
binding.searchStatus.clearFocus()
binding.searchFormat.clearFocus()
binding.searchSeason.clearFocus()
binding.searchYear.clearFocus()
updateChips()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -47,14 +102,149 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
exGenres = activity.result.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf()
setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener {
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
resetSearchFilter()
}
binding.resetSearchFilter.setOnLongClickListener {
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
val bounceAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
binding.resetSearchFilter.startAnimation(bounceAnimation)
binding.resetSearchFilter.postDelayed({
resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch {
activity.result.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.result.sort
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
excludedTags = exTags
}
activity.updateChips.invoke()
activity.search()
dismiss()
}
}, 500)
true
}
binding.sortByFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
startBounceZoomAnimation()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
startBounceZoomAnimation()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
startBounceZoomAnimation()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
startBounceZoomAnimation()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
startBounceZoomAnimation()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
startBounceZoomAnimation()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
startBounceZoomAnimation()
}
}
true
}
popupMenu.show()
}
binding.countryFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.country_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.country_global -> {
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_china -> {
activity.result.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_south_korea -> {
activity.result.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_japan -> {
activity.result.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_taiwan -> {
activity.result.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
}
true
}
popupMenu.show()
}
binding.searchFilterApply.setOnClickListener {
activity.result.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
sort = binding.searchSortBy.text.toString().ifBlank { null }
?.let { Anilist.sortBy[resources.getStringArray(R.array.sort_by).indexOf(it)] }
season = binding.searchSeason.text.toString().ifBlank { null }
seasonYear = binding.searchYear.text.toString().toIntOrNull()
if (activity.result.type == "ANIME") {
seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else {
startYear = binding.searchYear.text.toString().toIntOrNull()
}
sort = activity.result.sort
countryOfOrigin = activity.result.countryOfOrigin
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
@ -67,15 +257,22 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchFilterCancel.setOnClickListener {
dismiss()
}
binding.searchSortBy.setText(activity.result.sort?.let {
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
})
binding.searchSortBy.setAdapter(
val format = if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
binding.searchStatus.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
resources.getStringArray(R.array.sort_by)
format
)
)
binding.searchSource.setText(activity.result.source?.replace("_", " "))
binding.searchSource.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
Anilist.source.toTypedArray()
)
)
@ -84,11 +281,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
if (activity.result.type == "ANIME") {
binding.searchYear.setText(activity.result.seasonYear?.toString())
} else {
binding.searchYear.setText(activity.result.startYear?.toString())
}
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
else {
binding.searchSeason.setText(activity.result.season)
binding.searchSeason.setAdapter(
@ -98,16 +309,6 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
Anilist.seasons.toTypedArray()
)
)
binding.searchYear.setText(activity.result.seasonYear?.toString())
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
}
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->