Merge pull request #7310 from Bnyro/master

fix: scroll state not reset to latest video after refreshing feed/subscriptions
This commit is contained in:
Bnyro 2025-04-16 12:32:29 +02:00 committed by GitHub
commit c0ead845b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 52 additions and 45 deletions

View File

@ -43,11 +43,9 @@ 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.util.PlayingQueue
import com.github.libretube.util.deArrow
import com.google.android.material.chip.Chip
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_subscriptions) {
private var _binding: FragmentSubscriptionsBinding? = null
@ -138,12 +136,24 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
viewModel.fetchSubscriptions(requireContext())
}
// only restore the previous state (i.e. scroll position) the first time the feed is shown
// any other feed updates are caused by manual refreshing and thus should reset the scroll
// position to zero
var alreadyShowedFeedOnce = false
viewModel.videoFeed.observe(viewLifecycleOwner) {
if (!viewModel.isCurrentTabSubChannels && it != null) showFeed()
if (!viewModel.isCurrentTabSubChannels && it != null) {
showFeed(!alreadyShowedFeedOnce)
alreadyShowedFeedOnce = true
}
}
// restore the scroll position, same conditions as above
var alreadyShowedSubscriptionsOnce = false
viewModel.subscriptions.observe(viewLifecycleOwner) {
if (viewModel.isCurrentTabSubChannels && it != null) showSubscriptions()
if (viewModel.isCurrentTabSubChannels && it != null) {
showSubscriptions(!alreadyShowedSubscriptionsOnce)
alreadyShowedSubscriptionsOnce = true
}
}
viewModel.feedProgress.observe(viewLifecycleOwner) { progress ->
@ -209,7 +219,8 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
binding.subChannels.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
viewModel.subChannelsRecyclerViewState = binding.subChannels.layoutManager?.onSaveInstanceState()?.takeIf {
viewModel.subChannelsRecyclerViewState =
binding.subChannels.layoutManager?.onSaveInstanceState()?.takeIf {
binding.subChannels.computeVerticalScrollOffset() != 0
}
}
@ -218,7 +229,8 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
binding.subFeed.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
viewModel.subFeedRecyclerViewState = binding.subFeed.layoutManager?.onSaveInstanceState()?.takeIf {
viewModel.subFeedRecyclerViewState =
binding.subFeed.layoutManager?.onSaveInstanceState()?.takeIf {
binding.subFeed.computeVerticalScrollOffset() != 0
}
}
@ -235,29 +247,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
}
}
private fun loadFeedItems(sortedFeed: List<StreamItem>) {
val binding = _binding ?: return
if (viewModel.videoFeed.value != null && !viewModel.isCurrentTabSubChannels && !binding.subRefresh.isRefreshing) {
binding.subRefresh.isRefreshing = true
lifecycleScope.launch {
val streamItemsToInsert = sortedFeed.let {
withContext(Dispatchers.IO) {
runCatching { it.deArrow() }.getOrDefault(it)
}
}
feedAdapter.submitList(streamItemsToInsert) {
// manually restore the previous feed state
binding.subFeed.layoutManager?.onRestoreInstanceState(viewModel.subFeedRecyclerViewState)
binding.subscriptionsAppBar.setExpanded(viewModel.subFeedRecyclerViewState == null)
}
binding.subRefresh.isRefreshing = false
}
}
}
private fun setupSortAndFilter() {
binding.filterSort.setOnClickListener {
childFragmentManager.setFragmentResultListener(
@ -371,10 +360,10 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
else -> this
}
private fun showFeed() {
private fun showFeed(restoreScrollState: Boolean = true) {
val binding = _binding ?: return
val videoFeed = viewModel.videoFeed.value ?: return
binding.subRefresh.isRefreshing = false
val feed = videoFeed
.filterByGroup(selectedFilterGroup)
.filter { showUpcoming || !it.isUpcoming }
@ -382,16 +371,17 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
DatabaseHelper.filterByStatusAndWatchPosition(it, hideWatched)
}
val sorted = feed
val sortedFeed = feed
.sortedBySelectedOrder()
.toMutableList()
// add an "all caught up item"
if (selectedSortOrder == 0) {
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime()
val caughtUpIndex = feed.indexOfFirst { it.uploaded <= lastCheckedFeedTime && !it.isUpcoming }
val caughtUpIndex =
feed.indexOfFirst { it.uploaded <= lastCheckedFeedTime && !it.isUpcoming }
if (caughtUpIndex > 0 && !feed[caughtUpIndex - 1].isUpcoming) {
sorted.add(
sortedFeed.add(
caughtUpIndex,
StreamItem(type = VideoCardsAdapter.CAUGHT_UP_STREAM_TYPE)
)
@ -404,18 +394,30 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
val notLoaded = viewModel.videoFeed.value.isNullOrEmpty()
binding.subFeed.isGone = notLoaded
binding.emptyFeed.isVisible = notLoaded
loadFeedItems(sorted)
binding.toggleSubs.text = getString(R.string.subscriptions)
feed.firstOrNull { !it.isUpcoming }?.uploaded?.let {
PreferenceHelper.setLastFeedWatchedTime(it)
}
binding.subRefresh.isRefreshing = false
feedAdapter.submitList(sortedFeed) {
if (restoreScrollState) {
// manually restore the previous feed state
binding.subFeed.layoutManager?.onRestoreInstanceState(viewModel.subFeedRecyclerViewState)
binding.subscriptionsAppBar.setExpanded(viewModel.subFeedRecyclerViewState == null)
} else {
binding.subFeed.scrollToPosition(0)
}
}
}
@SuppressLint("SetTextI18n")
private fun showSubscriptions() {
val subscriptions = viewModel.subscriptions.value?.filterByGroup(selectedFilterGroup) ?: return
private fun showSubscriptions(restoreScrollState: Boolean = true) {
val subscriptions =
viewModel.subscriptions.value?.filterByGroup(selectedFilterGroup) ?: return
val legacySubscriptions = PreferenceHelper.getBoolean(
PreferenceKeys.LEGACY_SUBSCRIPTIONS,
@ -424,8 +426,12 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
val adapter = if (legacySubscriptions) legacySubscriptionsAdapter else channelsAdapter
adapter.submitList(subscriptions) {
if (restoreScrollState) {
binding.subFeed.layoutManager?.onRestoreInstanceState(viewModel.subChannelsRecyclerViewState)
binding.subscriptionsAppBar.setExpanded(viewModel.subChannelsRecyclerViewState == null)
} else {
binding.subFeed.scrollToPosition(0)
}
}
binding.subRefresh.isRefreshing = false

View File

@ -15,6 +15,7 @@ import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.repo.FeedProgress
import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -33,7 +34,7 @@ class SubscriptionsViewModel : ViewModel() {
val videoFeed = try {
SubscriptionHelper.getFeed(forceRefresh = forceRefresh) { feedProgress ->
this@SubscriptionsViewModel.feedProgress.postValue(feedProgress)
}
}.let { runCatching { it.deArrow() }.getOrDefault(it) }
} catch (e: Exception) {
context.toastFromMainDispatcher(R.string.server_error)
Log.e(TAG(), e.toString())