From 612944586a7f810d09c646170c16ca4c3b2fe1f0 Mon Sep 17 00:00:00 2001 From: Vladimir Brankov Date: Sat, 9 May 2026 10:29:56 -0400 Subject: [PATCH 1/4] Add group channel filtering to Subscriptions page Tapping a channel group now selects it and filters the subscriptions grid to show only channels in that group. Tapping the already-selected group opens its recent videos feed. The selected group is highlighted with an accent-tinted card background. - Add getSubscriptionsForGroup() DAO query (INNER JOIN on group membership) - Route getSubscriptions() through the new query when a non-All group is selected - Track selected group in SubscriptionViewModel via BehaviorProcessor, making the subscriptions stream reactive to selection changes via switchMap - Add isSelected flag to FeedGroupCardItem/FeedGroupCardGridItem with visual highlight using a 15% accent colour blend on the card background - Refactor SubscriptionFragment carousel rebuild into rebuildCarousel() so it responds to both DB changes and selection changes Co-Authored-By: Claude Sonnet 4.6 --- .../database/subscription/SubscriptionDAO.kt | 12 ++++ .../subscription/SubscriptionFragment.kt | 69 +++++++++++++------ .../local/subscription/SubscriptionManager.kt | 3 + .../subscription/SubscriptionViewModel.kt | 19 ++++- .../item/FeedGroupCardGridItem.kt | 17 ++++- .../subscription/item/FeedGroupCardItem.kt | 17 ++++- 6 files changed, 113 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 353c7148e32..086551bc4c3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -68,6 +68,18 @@ abstract class SubscriptionDAO : BasicDAO { filter: String ): Flowable> + @RewriteQueriesToDropUnusedColumns + @Query( + """ + SELECT * FROM subscriptions s + INNER JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + WHERE fgs.group_id = :groupId + ORDER BY name COLLATE NOCASE ASC + """ + ) + abstract fun getSubscriptionsForGroup(groupId: Long): Flowable> + @Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable> diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index e9bb9d831e9..9fbe9bfcb75 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -68,6 +68,9 @@ class SubscriptionFragment : BaseStateFragment() { private lateinit var feedGroupsSortMenuItem: GroupsHeader private val subscriptionsSection = Section() + private var currentGroups: List = emptyList() + private var currentListViewMode: Boolean = true + @State @JvmField var itemsListState: Parcelable? = null @@ -210,6 +213,9 @@ class SubscriptionFragment : BaseStateFragment() { handleFeedGroups(groups, listViewMode) } } + viewModel.selectedGroupLiveData.observe(viewLifecycleOwner) { + rebuildCarousel() + } setupInitialLayout() } @@ -219,18 +225,32 @@ class SubscriptionFragment : BaseStateFragment() { carouselAdapter = GroupAdapter>() carouselAdapter.setOnItemClickListener { item, _ -> - when (item) { - is FeedGroupCardItem -> - NavigationHelper.openFeedFragment(fm, item.groupId, item.name) + val groupId = when (item) { + is FeedGroupCardItem -> item.groupId - is FeedGroupCardGridItem -> - NavigationHelper.openFeedFragment(fm, item.groupId, item.name) + is FeedGroupCardGridItem -> item.groupId - is FeedGroupAddNewItem -> + is FeedGroupAddNewItem -> { FeedGroupDialog.newInstance().show(fm, null) + return@setOnItemClickListener + } - is FeedGroupAddNewGridItem -> + is FeedGroupAddNewGridItem -> { FeedGroupDialog.newInstance().show(fm, null) + return@setOnItemClickListener + } + + else -> return@setOnItemClickListener + } + val name = when (item) { + is FeedGroupCardItem -> item.name + is FeedGroupCardGridItem -> item.name + else -> "" + } + if (groupId == viewModel.getSelectedGroupId()) { + NavigationHelper.openFeedFragment(fm, groupId, name) + } else { + viewModel.selectGroup(groupId) } } carouselAdapter.setOnItemLongClickListener { item, _ -> @@ -375,31 +395,40 @@ class SubscriptionFragment : BaseStateFragment() { feedGroupsCarousel.onRestoreInstanceState(feedGroupsCarouselState) feedGroupsCarouselState = null } + currentGroups = groups + currentListViewMode = listViewMode + rebuildCarousel() + } + private fun rebuildCarousel() { + val selectedId = viewModel.getSelectedGroupId() binding.itemsList.post { - if (context == null) { - // since this part was posted to the next UI cycle, the fragment might have been - // removed in the meantime - return@post - } + if (context == null) return@post - feedGroupsCarousel.listViewMode = listViewMode - feedGroupsSortMenuItem.showSortButton = groups.size > 1 - feedGroupsSortMenuItem.listViewMode = listViewMode + feedGroupsCarousel.listViewMode = currentListViewMode + feedGroupsSortMenuItem.showSortButton = currentGroups.size > 1 + feedGroupsSortMenuItem.listViewMode = currentListViewMode feedGroupsCarousel.notifyChanged(FeedGroupCarouselItem.PAYLOAD_UPDATE_LIST_VIEW_MODE) feedGroupsSortMenuItem.notifyChanged(GroupsHeader.PAYLOAD_UPDATE_ICONS) - // update items here to prevent flickering carouselAdapter.apply { clear() - if (listViewMode) { + if (currentListViewMode) { add(FeedGroupAddNewItem()) - add(FeedGroupCardItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.WHATS_NEW)) + add(FeedGroupCardItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.WHATS_NEW, selectedId == GROUP_ALL_ID)) } else { add(FeedGroupAddNewGridItem()) - add(FeedGroupCardGridItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.WHATS_NEW)) + add(FeedGroupCardGridItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.WHATS_NEW, selectedId == GROUP_ALL_ID)) } - addAll(groups) + addAll( + currentGroups.map { group -> + when (group) { + is FeedGroupCardItem -> group.copy(isSelected = group.groupId == selectedId) + is FeedGroupCardGridItem -> group.copy(isSelected = group.groupId == selectedId) + else -> group + } + } + ) } } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 5cf378cc39f..210c9b6c26f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -46,6 +46,9 @@ class SubscriptionManager(context: Context) { showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) + currentGroupId != FeedGroupEntity.GROUP_ALL_ID -> + subscriptionTable.getSubscriptionsForGroup(currentGroupId) + else -> subscriptionTable.getAll() } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt index fc28f8e597b..5e5f611830c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt @@ -10,6 +10,8 @@ import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.schedulers.Schedulers import java.util.concurrent.TimeUnit +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID import org.schabi.newpipe.info_list.ItemViewMode import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.subscription.item.ChannelItem @@ -28,10 +30,14 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica ) private val listViewModeFlowable = listViewMode.distinctUntilChanged() + private val selectedGroupId = BehaviorProcessor.createDefault(GROUP_ALL_ID) + private val mutableStateLiveData = MutableLiveData() private val mutableFeedGroupsLiveData = MutableLiveData, Boolean>>() + private val mutableSelectedGroupLiveData = MutableLiveData(GROUP_ALL_ID) val stateLiveData: LiveData = mutableStateLiveData val feedGroupsLiveData: LiveData, Boolean>> = mutableFeedGroupsLiveData + val selectedGroupLiveData: LiveData = mutableSelectedGroupLiveData private var feedGroupItemsDisposable = Flowable .combineLatest( @@ -52,10 +58,12 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } ) - private var stateItemsDisposable = subscriptionManager.subscriptions() + private var stateItemsDisposable = selectedGroupId + .switchMap { groupId -> + subscriptionManager.getSubscriptions(groupId).subscribeOn(Schedulers.io()) + } .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } } - .subscribeOn(Schedulers.io()) .subscribe( { mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) }, { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } @@ -75,6 +83,13 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica return listViewMode.value ?: true } + fun selectGroup(groupId: Long) { + selectedGroupId.onNext(groupId) + mutableSelectedGroupLiveData.postValue(groupId) + } + + fun getSelectedGroupId(): Long = selectedGroupId.value ?: GROUP_ALL_ID + sealed class SubscriptionState { data class LoadedState(val subscriptions: List) : SubscriptionState() data class ErrorState(val error: Throwable? = null) : SubscriptionState() diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt index c78801c036e..51ba9807d82 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt @@ -1,16 +1,19 @@ package org.schabi.newpipe.local.subscription.item import android.view.View +import androidx.core.graphics.ColorUtils import com.xwray.groupie.viewbinding.BindableItem import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.databinding.FeedGroupCardGridItemBinding import org.schabi.newpipe.local.subscription.FeedGroupIcon +import org.schabi.newpipe.util.ThemeHelper data class FeedGroupCardGridItem( val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, val name: String, - val icon: FeedGroupIcon + val icon: FeedGroupIcon, + val isSelected: Boolean = false ) : BindableItem() { constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon) @@ -26,6 +29,18 @@ data class FeedGroupCardGridItem( override fun bind(viewBinding: FeedGroupCardGridItemBinding, position: Int) { viewBinding.title.text = name viewBinding.icon.setImageResource(icon.getDrawableRes()) + val context = viewBinding.root.context + viewBinding.root.setCardBackgroundColor( + if (isSelected) { + ColorUtils.blendARGB( + ThemeHelper.resolveColorFromAttr(context, R.attr.card_item_background_color), + ThemeHelper.resolveColorFromAttr(context, android.R.attr.colorAccent), + 0.15f + ) + } else { + ThemeHelper.resolveColorFromAttr(context, R.attr.card_item_background_color) + } + ) } override fun initializeViewBinding(view: View) = FeedGroupCardGridItemBinding.bind(view) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt index 7b78b3d955a..67484428091 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt @@ -1,16 +1,19 @@ package org.schabi.newpipe.local.subscription.item import android.view.View +import androidx.core.graphics.ColorUtils import com.xwray.groupie.viewbinding.BindableItem import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.databinding.FeedGroupCardItemBinding import org.schabi.newpipe.local.subscription.FeedGroupIcon +import org.schabi.newpipe.util.ThemeHelper data class FeedGroupCardItem( val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, val name: String, - val icon: FeedGroupIcon + val icon: FeedGroupIcon, + val isSelected: Boolean = false ) : BindableItem() { constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon) @@ -26,6 +29,18 @@ data class FeedGroupCardItem( override fun bind(viewBinding: FeedGroupCardItemBinding, position: Int) { viewBinding.title.text = name viewBinding.icon.setImageResource(icon.getDrawableRes()) + val context = viewBinding.root.context + viewBinding.root.setCardBackgroundColor( + if (isSelected) { + ColorUtils.blendARGB( + ThemeHelper.resolveColorFromAttr(context, R.attr.card_item_background_color), + ThemeHelper.resolveColorFromAttr(context, android.R.attr.colorAccent), + 0.15f + ) + } else { + ThemeHelper.resolveColorFromAttr(context, R.attr.card_item_background_color) + } + ) } override fun initializeViewBinding(view: View) = FeedGroupCardItemBinding.bind(view) From a54af548cfcbc5808b0bd3353f4f410231393b88 Mon Sep 17 00:00:00 2001 From: Vladimir Brankov Date: Sat, 9 May 2026 12:48:44 -0400 Subject: [PATCH 2/4] Add Ungrouped virtual group to subscription groups carousel Selecting the new "Ungrouped" group filters the subscriptions grid to show only channels that don't belong to any group, making it easy to see which channels still need to be categorised. - Add GROUP_UNGROUPED_ID = -2L constant to FeedGroupEntity - Add getSubscriptionsNotInAnyGroup() DAO query (LEFT JOIN, IS NULL) - Route getSubscriptions() to that query for GROUP_UNGROUPED_ID - Add "Ungrouped" card at the end of the groups carousel - Add feed_group_ungrouped string resource Co-Authored-By: Claude Sonnet 4.6 --- .../newpipe/database/feed/model/FeedGroupEntity.kt | 1 + .../newpipe/database/subscription/SubscriptionDAO.kt | 12 ++++++++++++ .../local/subscription/SubscriptionFragment.kt | 6 ++++++ .../local/subscription/SubscriptionManager.kt | 3 +++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 23 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt index 1dd26946a96..290c7e2d43f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt @@ -35,5 +35,6 @@ data class FeedGroupEntity( const val SORT_ORDER = "sort_order" const val GROUP_ALL_ID = -1L + const val GROUP_UNGROUPED_ID = -2L } } diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 086551bc4c3..2ff13dd0e43 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -80,6 +80,18 @@ abstract class SubscriptionDAO : BasicDAO { ) abstract fun getSubscriptionsForGroup(groupId: Long): Flowable> + @RewriteQueriesToDropUnusedColumns + @Query( + """ + SELECT * FROM subscriptions s + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + WHERE fgs.subscription_id IS NULL + ORDER BY name COLLATE NOCASE ASC + """ + ) + abstract fun getSubscriptionsNotInAnyGroup(): Flowable> + @Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable> diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 9fbe9bfcb75..562c6c964f8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -25,6 +25,7 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder import io.reactivex.rxjava3.disposables.CompositeDisposable import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID +import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_UNGROUPED_ID import org.schabi.newpipe.databinding.DialogTitleBinding import org.schabi.newpipe.databinding.FeedItemCarouselBinding import org.schabi.newpipe.databinding.FragmentSubscriptionBinding @@ -429,6 +430,11 @@ class SubscriptionFragment : BaseStateFragment() { } } ) + if (currentListViewMode) { + add(FeedGroupCardItem(GROUP_UNGROUPED_ID, getString(R.string.feed_group_ungrouped), FeedGroupIcon.PERSON, selectedId == GROUP_UNGROUPED_ID)) + } else { + add(FeedGroupCardGridItem(GROUP_UNGROUPED_ID, getString(R.string.feed_group_ungrouped), FeedGroupIcon.PERSON, selectedId == GROUP_UNGROUPED_ID)) + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 210c9b6c26f..2fd0aa63109 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -46,6 +46,9 @@ class SubscriptionManager(context: Context) { showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) + currentGroupId == FeedGroupEntity.GROUP_UNGROUPED_ID -> + subscriptionTable.getSubscriptionsNotInAnyGroup() + currentGroupId != FeedGroupEntity.GROUP_ALL_ID -> subscriptionTable.getSubscriptionsForGroup(currentGroupId) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 207f1363f5f..8aaff775b26 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -731,6 +731,7 @@ Do you want to delete this group? New Show only ungrouped subscriptions + Ungrouped Feed Feed update threshold Time after last update before a subscription is considered outdated — %s From 72436460649ab5a1fb6149743148ad390ce18c79 Mon Sep 17 00:00:00 2001 From: Vladimir Brankov Date: Sat, 9 May 2026 13:15:53 -0400 Subject: [PATCH 3/4] Fix group edit dialog showing only already-assigned channels getSubscriptions() is used by FeedGroupDialog to display all channels with checkmarks on those in the group. Our group-filtering logic was incorrectly intercepting that call. Extract subscriptionsFilteredByGroup() as a separate method used only by the subscriptions page. Co-Authored-By: Claude Sonnet 4.6 --- .../local/subscription/SubscriptionManager.kt | 12 ++++++------ .../local/subscription/SubscriptionViewModel.kt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 2fd0aa63109..f09fe0ddd8e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -27,6 +27,12 @@ class SubscriptionManager(context: Context) { fun subscriptionTable(): SubscriptionDAO = subscriptionTable fun subscriptions() = subscriptionTable.getAll() + fun subscriptionsFilteredByGroup(groupId: Long): Flowable> = when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> subscriptionTable.getAll() + FeedGroupEntity.GROUP_UNGROUPED_ID -> subscriptionTable.getSubscriptionsNotInAnyGroup() + else -> subscriptionTable.getSubscriptionsForGroup(groupId) + } + fun getSubscriptions( currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID, filterQuery: String = "", @@ -46,12 +52,6 @@ class SubscriptionManager(context: Context) { showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) - currentGroupId == FeedGroupEntity.GROUP_UNGROUPED_ID -> - subscriptionTable.getSubscriptionsNotInAnyGroup() - - currentGroupId != FeedGroupEntity.GROUP_ALL_ID -> - subscriptionTable.getSubscriptionsForGroup(currentGroupId) - else -> subscriptionTable.getAll() } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt index 5e5f611830c..136dbb4eb3f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt @@ -60,7 +60,7 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica private var stateItemsDisposable = selectedGroupId .switchMap { groupId -> - subscriptionManager.getSubscriptions(groupId).subscribeOn(Schedulers.io()) + subscriptionManager.subscriptionsFilteredByGroup(groupId).subscribeOn(Schedulers.io()) } .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } } From 60a8ecb9c9c30d7cbb4d467c23241be301694c69 Mon Sep 17 00:00:00 2001 From: Vladimir Brankov Date: Sat, 9 May 2026 15:36:50 -0400 Subject: [PATCH 4/4] Fix bugs and edge cases found in code review - Block long-press on Ungrouped (virtual group) to prevent opening edit dialog with invalid ID; extract groupId before the guard check - Block feed navigation on double-tap of Ungrouped (no feed exists for virtual groups); second tap re-selects instead of navigating - Move selectedId read inside post{} to avoid stale capture before the Runnable executes - Handle GROUP_UNGROUPED_ID in getId() alongside GROUP_ALL_ID - Replace LEFT JOIN + IS NULL with NOT EXISTS for getSubscriptionsNotInAnyGroup (cleaner intent, avoids @RewriteQueriesToDropUnusedColumns) - Initialise currentListViewMode from ViewModel rather than hardcoded true Co-Authored-By: Claude Sonnet 4.6 --- .../database/subscription/SubscriptionDAO.kt | 8 +++---- .../subscription/SubscriptionFragment.kt | 24 +++++++++---------- .../item/FeedGroupCardGridItem.kt | 2 +- .../subscription/item/FeedGroupCardItem.kt | 2 +- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 2ff13dd0e43..e156e7956ba 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -80,13 +80,13 @@ abstract class SubscriptionDAO : BasicDAO { ) abstract fun getSubscriptionsForGroup(groupId: Long): Flowable> - @RewriteQueriesToDropUnusedColumns @Query( """ SELECT * FROM subscriptions s - LEFT JOIN feed_group_subscription_join fgs - ON s.uid = fgs.subscription_id - WHERE fgs.subscription_id IS NULL + WHERE NOT EXISTS ( + SELECT 1 FROM feed_group_subscription_join fgs + WHERE fgs.subscription_id = s.uid + ) ORDER BY name COLLATE NOCASE ASC """ ) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 562c6c964f8..d9fdde98623 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -70,7 +70,7 @@ class SubscriptionFragment : BaseStateFragment() { private val subscriptionsSection = Section() private var currentGroups: List = emptyList() - private var currentListViewMode: Boolean = true + private var currentListViewMode: Boolean = true // updated from ViewModel after init @State @JvmField @@ -208,6 +208,7 @@ class SubscriptionFragment : BaseStateFragment() { binding.emptyStateView.setEmptyStateComposable() viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java] + currentListViewMode = viewModel.getListViewMode() viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) } viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let { (groups, listViewMode) -> @@ -248,26 +249,23 @@ class SubscriptionFragment : BaseStateFragment() { is FeedGroupCardGridItem -> item.name else -> "" } - if (groupId == viewModel.getSelectedGroupId()) { + if (groupId == viewModel.getSelectedGroupId() && groupId != GROUP_UNGROUPED_ID) { NavigationHelper.openFeedFragment(fm, groupId, name) } else { viewModel.selectGroup(groupId) } } carouselAdapter.setOnItemLongClickListener { item, _ -> - if ((item is FeedGroupCardItem && item.groupId == GROUP_ALL_ID) || - (item is FeedGroupCardGridItem && item.groupId == GROUP_ALL_ID) - ) { + val groupId = when (item) { + is FeedGroupCardItem -> item.groupId + is FeedGroupCardGridItem -> item.groupId + else -> return@setOnItemLongClickListener false + } + if (groupId == GROUP_ALL_ID || groupId == GROUP_UNGROUPED_ID) { return@setOnItemLongClickListener false } - when (item) { - is FeedGroupCardItem -> - FeedGroupDialog.newInstance(item.groupId).show(fm, null) - - is FeedGroupCardGridItem -> - FeedGroupDialog.newInstance(item.groupId).show(fm, null) - } + FeedGroupDialog.newInstance(groupId).show(fm, null) return@setOnItemLongClickListener true } @@ -402,10 +400,10 @@ class SubscriptionFragment : BaseStateFragment() { } private fun rebuildCarousel() { - val selectedId = viewModel.getSelectedGroupId() binding.itemsList.post { if (context == null) return@post + val selectedId = viewModel.getSelectedGroupId() feedGroupsCarousel.listViewMode = currentListViewMode feedGroupsSortMenuItem.showSortButton = currentGroups.size > 1 feedGroupsSortMenuItem.listViewMode = currentListViewMode diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt index 51ba9807d82..78da5b7aa0f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt @@ -19,7 +19,7 @@ data class FeedGroupCardGridItem( override fun getId(): Long { return when (groupId) { - FeedGroupEntity.GROUP_ALL_ID -> super.getId() + FeedGroupEntity.GROUP_ALL_ID, FeedGroupEntity.GROUP_UNGROUPED_ID -> super.getId() else -> groupId } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt index 67484428091..c318ec50a5d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt @@ -19,7 +19,7 @@ data class FeedGroupCardItem( override fun getId(): Long { return when (groupId) { - FeedGroupEntity.GROUP_ALL_ID -> super.getId() + FeedGroupEntity.GROUP_ALL_ID, FeedGroupEntity.GROUP_UNGROUPED_ID -> super.getId() else -> groupId } }