feat: Support multiple filter selection (#5478)

Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com>
This commit is contained in:
RafaRamos 2024-01-12 14:56:17 +00:00 committed by GitHub
parent 0e960e1a6c
commit 444eb693d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 348 additions and 92 deletions

View File

@ -34,4 +34,5 @@ object IntentData {
const val bitmapUrl = "bitmapUrl" const val bitmapUrl = "bitmapUrl"
const val isCurrentlyPlaying = "isCurrentlyPlaying" const val isCurrentlyPlaying = "isCurrentlyPlaying"
const val isSubscribed = "isSubscribed" const val isSubscribed = "isSubscribed"
const val sortOptions = "sortOptions"
} }

View File

@ -130,7 +130,7 @@ object PreferenceKeys {
const val LAST_STREAM_VIDEO_ID = "last_stream_video_id" const val LAST_STREAM_VIDEO_ID = "last_stream_video_id"
const val LAST_WATCHED_FEED_TIME = "last_watched_feed_time" const val LAST_WATCHED_FEED_TIME = "last_watched_feed_time"
const val HIDE_WATCHED_FROM_FEED = "hide_watched_from_feed" const val HIDE_WATCHED_FROM_FEED = "hide_watched_from_feed"
const val SELECTED_FEED_FILTER = "filer_feed" const val SELECTED_FEED_FILTERS = "filter_feed"
const val FEED_SORT_ORDER = "sort_oder_feed" const val FEED_SORT_ORDER = "sort_oder_feed"
/** /**

View File

@ -0,0 +1,33 @@
package com.github.libretube.enums
import com.github.libretube.constants.PreferenceKeys.SELECTED_FEED_FILTERS
import com.github.libretube.helpers.PreferenceHelper
enum class ContentFilter {
VIDEOS,
SHORTS,
LIVESTREAMS;
fun isEnabled() = enabledFiltersSet.contains(ordinal.toString())
fun setState(enabled: Boolean) {
val newFilters = enabledFiltersSet
.apply {if (enabled) add(ordinal.toString()) else remove(ordinal.toString()) }
.joinToString(",")
PreferenceHelper.putString(SELECTED_FEED_FILTERS, newFilters)
}
companion object {
private val enabledFiltersSet get() = PreferenceHelper
.getString(
key = SELECTED_FEED_FILTERS,
defValue = entries.joinToString(",") { it.ordinal.toString() }
)
.split(',')
.toMutableSet()
}
}

View File

@ -0,0 +1,10 @@
package com.github.libretube.obj
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class SelectableOption(
val isSelected: Boolean,
val name: String
): Parcelable

View File

@ -23,6 +23,7 @@ import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentHomeBinding import com.github.libretube.databinding.FragmentHomeBinding
import com.github.libretube.db.DatabaseHelper import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.DatabaseHolder
import com.github.libretube.enums.ContentFilter
import com.github.libretube.helpers.LocaleHelper import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.helpers.PlayerHelper import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
@ -145,12 +146,13 @@ class HomeFragment : Fragment() {
} }
}.getOrNull()?.takeIf { it.isNotEmpty() } ?: return }.getOrNull()?.takeIf { it.isNotEmpty() } ?: return
} }
val allowShorts = ContentFilter.SHORTS.isEnabled()
val allowVideos = ContentFilter.VIDEOS.isEnabled()
val allowAll = (!allowShorts && !allowVideos)
var filteredFeed = feed.filter { var filteredFeed = feed.filter {
when (PreferenceHelper.getInt(PreferenceKeys.SELECTED_FEED_FILTER, 0)) { (allowShorts && it.isShort) || (allowVideos && !it.isShort) || allowAll
1 -> !it.isShort
2 -> it.isShort
else -> true
}
} }
if (PreferenceHelper.getBoolean(PreferenceKeys.HIDE_WATCHED_FROM_FEED, false)) { if (PreferenceHelper.getBoolean(PreferenceKeys.HIDE_WATCHED_FROM_FEED, false)) {
filteredFeed = runBlocking { DatabaseHelper.filterUnwatched(filteredFeed) } filteredFeed = runBlocking { DatabaseHelper.filterUnwatched(filteredFeed) }
@ -256,4 +258,4 @@ class HomeFragment : Fragment() {
private const val BOOKMARKS = "bookmarks" private const val BOOKMARKS = "bookmarks"
private const val PLAYLISTS = "playlists" private const val PLAYLISTS = "playlists"
} }
} }

View File

@ -6,6 +6,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -16,15 +18,18 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentSubscriptionsBinding import com.github.libretube.databinding.FragmentSubscriptionsBinding
import com.github.libretube.db.DatabaseHelper import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.DatabaseHolder
import com.github.libretube.enums.ContentFilter
import com.github.libretube.extensions.dpToPx import com.github.libretube.extensions.dpToPx
import com.github.libretube.extensions.formatShort import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.helpers.NavigationHelper import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.obj.SelectableOption
import com.github.libretube.ui.adapters.LegacySubscriptionAdapter import com.github.libretube.ui.adapters.LegacySubscriptionAdapter
import com.github.libretube.ui.adapters.SubscriptionChannelAdapter import com.github.libretube.ui.adapters.SubscriptionChannelAdapter
import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.adapters.VideosAdapter
@ -32,8 +37,10 @@ import com.github.libretube.ui.base.DynamicLayoutManagerFragment
import com.github.libretube.ui.models.EditChannelGroupsModel import com.github.libretube.ui.models.EditChannelGroupsModel
import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.models.SubscriptionsViewModel import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.ChannelGroupsSheet import com.github.libretube.ui.sheets.ChannelGroupsSheet
import com.github.libretube.ui.sheets.FilterSortBottomSheet
import com.github.libretube.ui.sheets.FilterSortBottomSheet.Companion.FILTER_SORT_REQUEST_KEY
import com.github.libretube.ui.sheets.FilterSortBottomSheet.Companion.SELECTED_SORT_OPTION_KEY
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -57,11 +64,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
PreferenceHelper.putInt(PreferenceKeys.FEED_SORT_ORDER, value) PreferenceHelper.putInt(PreferenceKeys.FEED_SORT_ORDER, value)
field = value field = value
} }
private var selectedFilter = PreferenceHelper.getInt(PreferenceKeys.SELECTED_FEED_FILTER, 0)
set(value) {
PreferenceHelper.putInt(PreferenceKeys.SELECTED_FEED_FILTER, value)
field = value
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -84,9 +86,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
false false
) )
// update the text according to the current order and filter setupSortAndFilter()
binding.sortTV.text = resources.getStringArray(R.array.sortOptions)[selectedSortOrder]
binding.filterTV.text = resources.getStringArray(R.array.filterOptions)[selectedFilter]
binding.subRefresh.isEnabled = true binding.subRefresh.isEnabled = true
binding.subProgress.isVisible = true binding.subProgress.isVisible = true
@ -109,30 +109,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
viewModel.fetchFeed(requireContext()) viewModel.fetchFeed(requireContext())
} }
binding.sortTV.setOnClickListener {
val sortOptions = resources.getStringArray(R.array.sortOptions)
BaseBottomSheet().apply {
setSimpleItems(sortOptions.toList()) { index ->
binding.sortTV.text = sortOptions[index]
selectedSortOrder = index
showFeed()
}
}.show(childFragmentManager)
}
binding.filterTV.setOnClickListener {
val filterOptions = resources.getStringArray(R.array.filterOptions)
BaseBottomSheet().apply {
setSimpleItems(filterOptions.toList()) { index ->
binding.filterTV.text = filterOptions[index]
selectedFilter = index
showFeed()
}
}.show(childFragmentManager)
}
binding.toggleSubs.isVisible = true binding.toggleSubs.isVisible = true
binding.toggleSubs.setOnClickListener { binding.toggleSubs.setOnClickListener {
@ -196,6 +172,33 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
} }
} }
private fun setupSortAndFilter() {
binding.filterSort.setOnClickListener {
val activityCompat = context as AppCompatActivity
val fragManager = activityCompat
.supportFragmentManager
.apply {
setFragmentResultListener(FILTER_SORT_REQUEST_KEY, activityCompat) { _, resultBundle ->
selectedSortOrder = resultBundle.getInt(SELECTED_SORT_OPTION_KEY)
showFeed()
}
}
FilterSortBottomSheet()
.apply { arguments = bundleOf(IntentData.sortOptions to fetchSortOptions()) }
.show(fragManager)
}
}
private fun fetchSortOptions(): Array<SelectableOption> {
return resources
.getStringArray(R.array.sortOptions)
.mapIndexed { index, option ->
SelectableOption(isSelected = index == selectedSortOrder, name = option)
}
.toTypedArray()
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
@ -258,15 +261,17 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
} }
private fun List<StreamItem>.filterByStatusAndWatchPosition(): List<StreamItem> { private fun List<StreamItem>.filterByStatusAndWatchPosition(): List<StreamItem> {
val streamItems = this.filter { val streamItems = this.filter {
val isLive = (it.duration ?: -1L) < 0L val isVideo = !it.isShort && !it.isLive
when (selectedFilter) {
0 -> true return@filter when {
1 -> !it.isShort && !isLive !ContentFilter.SHORTS.isEnabled() && it.isShort -> false
2 -> it.isShort !ContentFilter.VIDEOS.isEnabled() && isVideo -> false
3 -> isLive !ContentFilter.LIVESTREAMS.isEnabled() && it.isLive -> false
else -> throw IllegalArgumentException() else -> true
} }
} }
if (!PreferenceHelper.getBoolean( if (!PreferenceHelper.getBoolean(

View File

@ -0,0 +1,106 @@
package com.github.libretube.ui.sheets
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.FilterSortSheetBinding
import com.github.libretube.enums.ContentFilter
import com.github.libretube.obj.SelectableOption
class FilterSortBottomSheet: ExpandedBottomSheet() {
private var _binding: FilterSortSheetBinding? = null
private val binding get() = _binding!!
private lateinit var sortOptions: Array<SelectableOption>
private var selectedIndex: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
sortOptions = requireArguments().getParcelableArray(IntentData.sortOptions) as Array<SelectableOption>
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FilterSortSheetBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
addSortOptions()
observeSortChanges()
setInitialFiltersState()
observeFiltersChanges()
}
private fun addSortOptions() {
for (i in sortOptions.indices) {
val option = sortOptions.elementAt(i)
val rb = createRadioButton(i, option.name)
binding.sortRadioGroup.addView(rb)
if (option.isSelected) {
selectedIndex = i
binding.sortRadioGroup.check(rb.id)
}
}
}
private fun createRadioButton(index: Int, name: String): RadioButton {
return RadioButton(context).apply {
tag = index
text = name
}
}
private fun observeSortChanges() {
binding.sortRadioGroup.setOnCheckedChangeListener { group, checkedId ->
val index = group.findViewById<RadioButton>(checkedId).tag as Int
selectedIndex = index
notifyChange()
}
}
private fun setInitialFiltersState() {
binding.filterVideos.isChecked = ContentFilter.VIDEOS.isEnabled()
binding.filterShorts.isChecked = ContentFilter.SHORTS.isEnabled()
binding.filterLivestreams.isChecked = ContentFilter.LIVESTREAMS.isEnabled()
}
private fun observeFiltersChanges() {
binding.filters.setOnCheckedStateChangeListener { _, _ ->
ContentFilter.VIDEOS.setState(binding.filterVideos.isChecked)
ContentFilter.SHORTS.setState(binding.filterShorts.isChecked)
ContentFilter.LIVESTREAMS.setState(binding.filterLivestreams.isChecked)
notifyChange()
}
}
private fun notifyChange() {
setFragmentResult(
requestKey = FILTER_SORT_REQUEST_KEY,
result = bundleOf(SELECTED_SORT_OPTION_KEY to selectedIndex)
)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val FILTER_SORT_REQUEST_KEY = "filter_sort_request_key"
const val SELECTED_SORT_OPTION_KEY = "selected_sort_option_key"
}
}

View File

@ -0,0 +1,12 @@
<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="M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
</vector>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<FrameLayout
android:id="@+id/standard_bottom_sheet"
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="20dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Drag handle for accessibility -->
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="4dp"
android:text="@string/tooltip_filter" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/filters"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
app:selectionRequired="true"
app:singleLine="false"
app:singleSelection="false"
app:chipMinTouchTargetSize="0dp">
<com.google.android.material.chip.Chip
android:id="@+id/filter_videos"
style="@style/ElevatedFilterChip"
android:layout_marginHorizontal="0dp"
app:chipSpacingVertical="10dp"
app:chipEndPadding="6dp"
app:chipStartPadding="6dp"
android:checked="true"
android:text="@string/videos" />
<com.google.android.material.chip.Chip
android:id="@+id/filter_shorts"
style="@style/ElevatedFilterChip"
android:layout_marginHorizontal="0dp"
app:chipEndPadding="6dp"
app:chipStartPadding="6dp"
android:checked="true"
android:text="@string/yt_shorts" />
<com.google.android.material.chip.Chip
android:id="@+id/filter_livestreams"
style="@style/ElevatedFilterChip"
android:layout_marginHorizontal="0dp"
app:chipEndPadding="6dp"
app:chipStartPadding="6dp"
android:checked="true"
android:text="@string/livestreams" />
</com.google.android.material.chip.ChipGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="4dp"
android:text="@string/tooltip_sort" />
<RadioGroup
android:id="@+id/sort_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -55,19 +55,41 @@
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.button.MaterialButton <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toggle_subs"
style="@style/PlayerActionsButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_marginHorizontal="14dp"
android:layout_marginTop="5dp" <ImageView
android:layout_marginBottom="12dp" android:id="@+id/filter_sort"
android:text="@string/subscriptions" android:layout_width="40dp"
android:textAlignment="viewStart" android:layout_height="40dp"
android:textColor="?colorPrimary" android:src="@drawable/ic_filter_sort"
app:drawableEndCompat="@drawable/ic_arrow_up_down" android:contentDescription="@string/tooltip_filter"
app:drawableTint="?colorPrimary" /> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:padding="6dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="7dp"
android:alpha="0.7"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/toggle_subs"
style="@style/PlayerActionsButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="6dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="6dp"
app:layout_constraintEnd_toStartOf="@id/filter_sort"
app:layout_constraintStart_toStartOf="parent"
android:text="@string/subscriptions"
android:textAlignment="viewStart"
android:textColor="?colorPrimary"
app:drawableEndCompat="@drawable/ic_arrow_up_down"
app:drawableTint="?colorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout <RelativeLayout
android:id="@+id/sub_channels_container" android:id="@+id/sub_channels_container"
@ -90,39 +112,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp">
<TextView
android:id="@+id/filterTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="5dp"
android:drawablePadding="5dp"
android:paddingHorizontal="10dp"
android:text="@string/all"
android:textSize="16sp"
android:tooltipText="@string/tooltip_filter"
app:drawableEndCompat="@drawable/ic_filter" />
<TextView
android:id="@+id/sortTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="5dp"
android:drawablePadding="5dp"
android:paddingHorizontal="10dp"
android:text="@string/most_recent"
android:textSize="16sp"
android:tooltipText="@string/tooltip_sort"
app:drawableEndCompat="@drawable/ic_sort" />
</FrameLayout>
<HorizontalScrollView <HorizontalScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"