diff --git a/Habitica/res/layout/bottom_sheet_backgrounds_filter.xml b/Habitica/res/layout/bottom_sheet_backgrounds_filter.xml
new file mode 100644
index 000000000..ef354fca9
--- /dev/null
+++ b/Habitica/res/layout/bottom_sheet_backgrounds_filter.xml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Habitica/res/menu/menu_list_customizations.xml b/Habitica/res/menu/menu_list_customizations.xml
new file mode 100644
index 000000000..936b67bd8
--- /dev/null
+++ b/Habitica/res/menu/menu_list_customizations.xml
@@ -0,0 +1,9 @@
+
diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml
index 8c3dae0a4..565c5dd9d 100644
--- a/Habitica/res/values/strings.xml
+++ b/Habitica/res/values/strings.xml
@@ -1241,4 +1241,22 @@
20% Experience points
The amount gained varies randomly from 10 to 50
Day Start Adjustment
+ Purchased
+ Show Me
+ Background Filters
+ Newest
+ Oldest
+ Sort By
+ January
+ Febuary
+ March
+ April
+ May
+ June
+ July
+ August
+ September
+ October
+ November
+ December
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/CustomizationFilter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/CustomizationFilter.kt
new file mode 100644
index 000000000..460dcc503
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/CustomizationFilter.kt
@@ -0,0 +1,12 @@
+package com.habitrpg.android.habitica.models
+
+data class CustomizationFilter(
+ var onlyPurchased: Boolean = false,
+ var ascending: Boolean = false,
+ var months: MutableList = mutableListOf()
+) {
+ val isFiltering: Boolean
+ get() {
+ return onlyPurchased || months.isNotEmpty()
+ }
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt
index 31328b5ee..3fcb88363 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/inventory/customization/AvatarCustomizationFragment.kt
@@ -2,22 +2,35 @@ package com.habitrpg.android.habitica.ui.fragments.inventory.customization
import android.os.Bundle
import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import android.widget.CheckBox
import androidx.recyclerview.widget.GridLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.CustomizationRepository
import com.habitrpg.android.habitica.data.InventoryRepository
+import com.habitrpg.android.habitica.databinding.BottomSheetBackgroundsFilterBinding
import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
import com.habitrpg.android.habitica.helpers.RxErrorHandler
+import com.habitrpg.android.habitica.models.CustomizationFilter
+import com.habitrpg.android.habitica.models.inventory.Customization
+import com.habitrpg.android.habitica.models.user.OwnedCustomization
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.adapter.CustomizationRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.helpers.MarginDecoration
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
+import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog
+import io.reactivex.rxjava3.core.BackpressureStrategy
+import io.reactivex.rxjava3.kotlin.combineLatest
+import io.reactivex.rxjava3.subjects.BehaviorSubject
+import io.reactivex.rxjava3.subjects.PublishSubject
import javax.inject.Inject
class AvatarCustomizationFragment :
@@ -44,6 +57,9 @@ class AvatarCustomizationFragment :
internal var adapter: CustomizationRecyclerViewAdapter = CustomizationRecyclerViewAdapter()
internal var layoutManager: GridLayoutManager = GridLayoutManager(activity, 2)
+ private val currentFilter = BehaviorSubject.create()
+ private val ownedCustomizations = PublishSubject.create>()
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -115,6 +131,7 @@ class AvatarCustomizationFragment :
this.loadCustomizations()
userViewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
+ currentFilter.onNext(CustomizationFilter())
}
override fun onDestroy() {
@@ -122,6 +139,22 @@ class AvatarCustomizationFragment :
super.onDestroy()
}
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.menu_list_customizations, menu)
+ }
+
+ @Suppress("ReturnCount")
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_filter -> {
+ showFilterDialog()
+ return true
+ }
+ }
+
+ return super.onOptionsItemSelected(item)
+ }
+
override fun injectFragment(component: UserComponent) {
component.inject(this)
}
@@ -129,10 +162,44 @@ class AvatarCustomizationFragment :
private fun loadCustomizations() {
val type = this.type ?: return
compositeSubscription.add(
- customizationRepository.getCustomizations(type, category, false).subscribe(
- {
- adapter.setCustomizations(if (type == "background") { it.reversed() } else { it })
- },
+ customizationRepository.getCustomizations(type, category, false)
+ .combineLatest(currentFilter.toFlowable(BackpressureStrategy.DROP),
+ ownedCustomizations.toFlowable(BackpressureStrategy.DROP))
+ .subscribe(
+ { (customizations, filter, ownedCustomizations) ->
+ if (filter.isFiltering) {
+ val displayedCustomizations = mutableListOf()
+ for (customization in customizations) {
+ if (filter.onlyPurchased) {
+ if (ownedCustomizations.find { it.key == customization.identifier } == null) {
+ continue
+ }
+ }
+ if (filter.months.isNotEmpty()) {
+ if (!filter.months.contains(customization.customizationSetName?.substringAfter('.'))) {
+ continue
+ }
+ }
+ displayedCustomizations.add(customization)
+ }
+ adapter.setCustomizations(
+ if (!filter.ascending) {
+ displayedCustomizations.reversed()
+ } else {
+ displayedCustomizations
+ }
+ )
+ } else {
+ adapter.setCustomizations(
+ if (!filter.ascending) {
+ customizations.reversed()
+ } else {
+ customizations
+ }
+ )
+ }
+ adapter.ownedCustomizations = ownedCustomizations.map { it.key + "_" + it.type + "_" + it.category }
+ },
RxErrorHandler.handleEmptyError()
)
)
@@ -154,9 +221,7 @@ class AvatarCustomizationFragment :
fun updateUser(user: User?) {
if (user == null) return
this.updateActiveCustomization(user)
- val ownedCustomizations = ArrayList()
- user.purchased?.customizations?.filter { it.type == this.type && it.purchased }?.mapTo(ownedCustomizations) { it.key + "_" + it.type + "_" + it.category }
- adapter.updateOwnership(ownedCustomizations)
+ ownedCustomizations.onNext(user.purchased?.customizations?.filter { it.type == this.type && it.purchased })
this.adapter.userSize = user.preferences?.size
this.adapter.hairColor = user.preferences?.hair?.color
this.adapter.gemBalance = user.gemCount
@@ -192,7 +257,7 @@ class AvatarCustomizationFragment :
override fun onRefresh() {
compositeSubscription.add(
- userRepository.retrieveUser(false, true).subscribe(
+ userRepository.retrieveUser(withTasks = false, forced = true).subscribe(
{
binding?.refreshLayout?.isRefreshing = false
},
@@ -200,4 +265,59 @@ class AvatarCustomizationFragment :
)
)
}
+
+ fun showFilterDialog() {
+ val filter = currentFilter.value ?: CustomizationFilter()
+ val context = context ?: return
+ val dialog = HabiticaBottomSheetDialog(context)
+ val binding = BottomSheetBackgroundsFilterBinding.inflate(layoutInflater)
+ binding.showMeWrapper.check(if (filter.onlyPurchased) R.id.show_purchased_button else R.id.show_all_button)
+ binding.showMeWrapper.setOnCheckedChangeListener { _, checkedId ->
+ filter.onlyPurchased = checkedId == R.id.show_purchased_button
+ currentFilter.onNext(filter)
+ }
+ binding.clearButton.setOnClickListener {
+ currentFilter.onNext(CustomizationFilter())
+ dialog.dismiss()
+ }
+ if (type == "background") {
+ binding.sortByWrapper.check(if (filter.ascending) R.id.oldest_button else R.id.newest_button)
+ binding.sortByWrapper.setOnCheckedChangeListener { _, checkedId ->
+ filter.ascending = checkedId == R.id.oldest_button
+ currentFilter.onNext(filter)
+ }
+ configureMonthFilterButton(binding.januaryButton, 1, filter)
+ configureMonthFilterButton(binding.febuaryButton, 2, filter)
+ configureMonthFilterButton(binding.marchButton, 3, filter)
+ configureMonthFilterButton(binding.aprilButton, 4, filter)
+ configureMonthFilterButton(binding.mayButton, 5, filter)
+ configureMonthFilterButton(binding.juneButton, 6, filter)
+ configureMonthFilterButton(binding.julyButton, 7, filter)
+ configureMonthFilterButton(binding.augustButton, 8, filter)
+ configureMonthFilterButton(binding.septemberButton, 9, filter)
+ configureMonthFilterButton(binding.octoberButton, 10, filter)
+ configureMonthFilterButton(binding.novemberButton, 11, filter)
+ configureMonthFilterButton(binding.decemberButton, 12, filter)
+ } else {
+ binding.sortByTitle.visibility = View.GONE
+ binding.sortByWrapper.visibility = View.GONE
+ binding.monthReleasedTitle.visibility = View.GONE
+ binding.monthReleasedWrapper.visibility = View.GONE
+ }
+ dialog.setContentView(binding.root)
+ dialog.show()
+ }
+
+ private fun configureMonthFilterButton(button: CheckBox, value: Int, filter: CustomizationFilter) {
+ val identifier = value.toString().padStart(2, '0')
+ button.isChecked = filter.months.contains(identifier)
+ button.setOnCheckedChangeListener { _, isChecked ->
+ if (!isChecked && filter.months.contains(identifier)) {
+ filter.months.remove(identifier)
+ } else if (isChecked && !filter.months.contains(identifier)) {
+ filter.months.add(identifier)
+ }
+ currentFilter.onNext(filter)
+ }
+ }
}