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 isCurrentlyPlaying = "isCurrentlyPlaying"
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_WATCHED_FEED_TIME = "last_watched_feed_time"
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"
/**

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

View File

@ -6,6 +6,8 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -16,15 +18,18 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentSubscriptionsBinding
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.enums.ContentFilter
import com.github.libretube.extensions.dpToPx
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.NavigationHelper
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.SubscriptionChannelAdapter
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.PlayerViewModel
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.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.google.android.material.chip.Chip
import kotlinx.coroutines.Dispatchers
@ -57,11 +64,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
PreferenceHelper.putInt(PreferenceKeys.FEED_SORT_ORDER, 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(
inflater: LayoutInflater,
@ -84,9 +86,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
false
)
// update the text according to the current order and filter
binding.sortTV.text = resources.getStringArray(R.array.sortOptions)[selectedSortOrder]
binding.filterTV.text = resources.getStringArray(R.array.filterOptions)[selectedFilter]
setupSortAndFilter()
binding.subRefresh.isEnabled = true
binding.subProgress.isVisible = true
@ -109,30 +109,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
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.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() {
super.onDestroyView()
_binding = null
@ -258,15 +261,17 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment() {
}
private fun List<StreamItem>.filterByStatusAndWatchPosition(): List<StreamItem> {
val streamItems = this.filter {
val isLive = (it.duration ?: -1L) < 0L
when (selectedFilter) {
0 -> true
1 -> !it.isShort && !isLive
2 -> it.isShort
3 -> isLive
else -> throw IllegalArgumentException()
val isVideo = !it.isShort && !it.isLive
return@filter when {
!ContentFilter.SHORTS.isEnabled() && it.isShort -> false
!ContentFilter.VIDEOS.isEnabled() && isVideo -> false
!ContentFilter.LIVESTREAMS.isEnabled() && it.isLive -> false
else -> true
}
}
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:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/toggle_subs"
style="@style/PlayerActionsButton"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="14dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="12dp"
android:text="@string/subscriptions"
android:textAlignment="viewStart"
android:textColor="?colorPrimary"
app:drawableEndCompat="@drawable/ic_arrow_up_down"
app:drawableTint="?colorPrimary" />
android:layout_height="wrap_content">
<ImageView
android:id="@+id/filter_sort"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_filter_sort"
android:contentDescription="@string/tooltip_filter"
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
android:id="@+id/sub_channels_container"
@ -90,39 +112,6 @@
android:layout_height="match_parent"
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
android:layout_width="match_parent"
android:layout_height="wrap_content"