diff --git a/app/src/main/java/com/github/libretube/constants/IntentData.kt b/app/src/main/java/com/github/libretube/constants/IntentData.kt index 6f8628926..f5bd35433 100644 --- a/app/src/main/java/com/github/libretube/constants/IntentData.kt +++ b/app/src/main/java/com/github/libretube/constants/IntentData.kt @@ -34,4 +34,5 @@ object IntentData { const val bitmapUrl = "bitmapUrl" const val isCurrentlyPlaying = "isCurrentlyPlaying" const val isSubscribed = "isSubscribed" + const val sortOptions = "sortOptions" } diff --git a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt index e063d9546..77e1046f6 100644 --- a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt +++ b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt @@ -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" /** diff --git a/app/src/main/java/com/github/libretube/enums/ContentFilter.kt b/app/src/main/java/com/github/libretube/enums/ContentFilter.kt new file mode 100644 index 000000000..978bc084b --- /dev/null +++ b/app/src/main/java/com/github/libretube/enums/ContentFilter.kt @@ -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() + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/obj/SelectableOption.kt b/app/src/main/java/com/github/libretube/obj/SelectableOption.kt new file mode 100644 index 000000000..2378c3dea --- /dev/null +++ b/app/src/main/java/com/github/libretube/obj/SelectableOption.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt index 400b0ce1e..2698a0174 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt @@ -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" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt index b8dffb044..296f7a2cf 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt @@ -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 { + 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.filterByStatusAndWatchPosition(): List { + 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( diff --git a/app/src/main/java/com/github/libretube/ui/sheets/FilterSortBottomSheet.kt b/app/src/main/java/com/github/libretube/ui/sheets/FilterSortBottomSheet.kt new file mode 100644 index 000000000..da6a83ea1 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/sheets/FilterSortBottomSheet.kt @@ -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 + + private var selectedIndex: Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + sortOptions = requireArguments().getParcelableArray(IntentData.sortOptions) as Array + 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(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" + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_filter_sort.xml b/app/src/main/res/drawable/ic_filter_sort.xml new file mode 100644 index 000000000..01923547f --- /dev/null +++ b/app/src/main/res/drawable/ic_filter_sort.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/layout/filter_sort_sheet.xml b/app/src/main/res/layout/filter_sort_sheet.xml new file mode 100644 index 000000000..89c2495af --- /dev/null +++ b/app/src/main/res/layout/filter_sort_sheet.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml index 82c827c7c..aee88c2da 100644 --- a/app/src/main/res/layout/fragment_subscriptions.xml +++ b/app/src/main/res/layout/fragment_subscriptions.xml @@ -55,19 +55,41 @@ android:animateLayoutChanges="true" android:orientation="vertical"> - + android:layout_height="wrap_content"> + + + + + + - - - - - - - -