Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for feed pagination #3739

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions app/src/main/java/com/github/libretube/api/PipedApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.github.libretube.api.obj.Subscribe
import com.github.libretube.api.obj.Subscribed
import com.github.libretube.api.obj.Subscription
import com.github.libretube.api.obj.Token
import com.github.libretube.constants.FEED_PAGE_ITEMS_LIMIT
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
Expand Down Expand Up @@ -107,16 +108,24 @@ interface PipedApi {
)

@GET("feed")
suspend fun getFeed(@Query("authToken") token: String?): List<StreamItem>
suspend fun getFeed(
@Query("authToken") token: String?,
@Query("start") start: Long?,
@Query("limit") limit: Int? = FEED_PAGE_ITEMS_LIMIT
): List<StreamItem>

@GET("feed/unauthenticated")
suspend fun getUnauthenticatedFeed(
@Query("channels") channels: String
@Query("channels") channels: String,
@Query("start") start: Long?,
@Query("limit") limit: Int? = FEED_PAGE_ITEMS_LIMIT
): List<StreamItem>

@POST("feed/unauthenticated")
suspend fun getUnauthenticatedFeed(
@Body channels: List<String>
@Body channels: List<String>,
@Query("start") start: Long?,
@Query("limit") limit: Int? = FEED_PAGE_ITEMS_LIMIT
): List<StreamItem>

@GET("subscribed")
Expand Down
11 changes: 6 additions & 5 deletions app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,20 @@ object SubscriptionHelper {
}
}

suspend fun getFeed(): List<StreamItem> {
suspend fun getFeed(start: Long?): List<StreamItem> {
val token = PreferenceHelper.getToken()
return if (token.isNotEmpty()) {
RetrofitInstance.authApi.getFeed(token)
RetrofitInstance.authApi.getFeed(token, start)
} else {
val subscriptions = Database.localSubscriptionDao().getAll().map { it.channelId }
when {
subscriptions.size > GET_SUBSCRIPTIONS_LIMIT -> RetrofitInstance.authApi.getUnauthenticatedFeed(
subscriptions
subscriptions,
start
)

else -> RetrofitInstance.authApi.getUnauthenticatedFeed(
subscriptions.joinToString(",")
subscriptions.joinToString(","),
start
)
}
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/github/libretube/api/obj/StreamItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ data class StreamItem(
duration = duration
)
}

companion object {
const val CAUGHT_TYPE_KEY = "caught"
}
}
5 changes: 5 additions & 0 deletions app/src/main/java/com/github/libretube/constants/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,8 @@ const val DATABASE_NAME = "LibreTubeDatabase"
* New Streams notifications
*/
const val NOTIFICATION_WORK_NAME = "NotificationService"

/**
* Feed pagination limit per page
*/
const val FEED_PAGE_ITEMS_LIMIT = 20
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,10 @@ class VideosAdapter(
private val forceMode: ForceMode = ForceMode.NONE
) : RecyclerView.Adapter<VideosViewHolder>() {

private var visibleCount = minOf(10, streamItems.size)

override fun getItemCount(): Int {
return when {
showAllAtOnce -> streamItems.size
else -> minOf(streamItems.size, visibleCount)
}
}
override fun getItemCount() = streamItems.size

override fun getItemViewType(position: Int): Int {
return if (streamItems[position].type == "caught") CAUGHT_UP_TYPE else NORMAL_TYPE
}

fun updateItems() {
val oldSize = visibleCount
visibleCount += minOf(10, streamItems.size - oldSize)
if (visibleCount == oldSize) return
notifyItemRangeInserted(oldSize, visibleCount)
return if (streamItems[position].type == StreamItem.CAUGHT_TYPE_KEY) CAUGHT_UP_TYPE else NORMAL_TYPE
}

fun insertItems(newItems: List<StreamItem>) {
Expand All @@ -66,7 +52,6 @@ class VideosAdapter(
it.url?.toID() == videoId
}.takeIf { it > 0 } ?: return
streamItems.removeAt(index)
visibleCount -= 1
notifyItemRemoved(index)
notifyItemRangeChanged(index, itemCount)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class HomeFragment : Fragment() {
} else {
runCatching {
withContext(Dispatchers.IO) {
SubscriptionHelper.getFeed()
SubscriptionHelper.getFeed(null)
}
}.getOrNull()?.takeIf { it.isNotEmpty() } ?: return
}.filter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.libretube.ui.fragments

import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand Down Expand Up @@ -42,11 +43,6 @@ class SubscriptionsFragment : Fragment() {
private var selectedFilterGroup: Int = 0

var subscriptionsAdapter: VideosAdapter? = null
private var selectedSortOrder = PreferenceHelper.getInt(PreferenceKeys.FEED_SORT_ORDER, 0)
set(value) {
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)
Expand All @@ -65,19 +61,18 @@ class SubscriptionsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

subscriptionsAdapter = null

val loadFeedInBackground = PreferenceHelper.getBoolean(
PreferenceKeys.SAVE_FEED,
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]

binding.subRefresh.isEnabled = true

binding.subProgress.visibility = View.VISIBLE

binding.subFeed.layoutManager = VideosAdapter.getLayout(requireContext())

if (viewModel.videoFeed.value == null || !loadFeedInBackground) {
Expand All @@ -103,22 +98,11 @@ class SubscriptionsFragment : Fragment() {
}

binding.subRefresh.setOnRefreshListener {
subscriptionsAdapter = null
viewModel.fetchSubscriptions()
viewModel.fetchFeed()
}

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)

Expand Down Expand Up @@ -152,11 +136,14 @@ class SubscriptionsFragment : Fragment() {
binding.scrollviewSub.viewTreeObserver.addOnScrollChangedListener {
val binding = _binding
if (binding?.scrollviewSub?.canScrollVertically(1) == false &&
viewModel.videoFeed.value != null // scroll view is at bottom
!viewModel.videoFeed.value.isNullOrEmpty()
) {
// a start parameter is required for the next page
// if there's no other video available to get it from, the feed might be empty in general
// and hence shouldn't be updated
val start = viewModel.videoFeed.value?.lastOrNull()?.uploaded ?: return@addOnScrollChangedListener
binding.subRefresh.isRefreshing = true
subscriptionsAdapter?.updateItems()
binding.subRefresh.isRefreshing = false
viewModel.fetchFeed(start)
}
}

Expand All @@ -170,7 +157,6 @@ class SubscriptionsFragment : Fragment() {
_binding = null
}

@SuppressLint("InflateParams")
private suspend fun initChannelGroups() {
channelGroups = DatabaseHolder.Database.subscriptionGroupsDao().getAll()

Expand Down Expand Up @@ -203,10 +189,11 @@ class SubscriptionsFragment : Fragment() {
}
}

private fun showFeed() {
if (viewModel.videoFeed.value == null) return
private fun showFeed() = lifecycleScope.launch {
Log.e("show feed", viewModel.videoFeed.value.orEmpty().size.toString())

binding.subRefresh.isRefreshing = false
binding.subProgress.isGone = true
val feed = viewModel.videoFeed.value!!
.filter { streamItem ->
// filter for selected channel groups
Expand Down Expand Up @@ -240,28 +227,15 @@ class SubscriptionsFragment : Fragment() {
removeWatchVideosFromFeed(streams)
}
}
}

// sort the feed
val sortedFeed = when (selectedSortOrder) {
0 -> feed
1 -> feed.reversed()
2 -> feed.sortedBy { it.views }.reversed()
3 -> feed.sortedBy { it.views }
4 -> feed.sortedBy { it.uploaderName }
5 -> feed.sortedBy { it.uploaderName }.reversed()
else -> feed
}.toMutableList()
}.toMutableList()

// add an "all caught up item"
if (selectedSortOrder == 0) {
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime()
val caughtUpIndex = feed.indexOfFirst {
(it.uploaded ?: 0L) / 1000 < lastCheckedFeedTime
}
if (caughtUpIndex > 0) {
sortedFeed.add(caughtUpIndex, StreamItem(type = "caught"))
}
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime()
val caughtUpIndex = feed.indexOfFirst {
(it.uploaded ?: 0L) / 1000 < lastCheckedFeedTime
}
if (caughtUpIndex > 0) {
feed.add(caughtUpIndex, StreamItem(type = StreamItem.CAUGHT_TYPE_KEY))
}

binding.subChannelsContainer.visibility = View.GONE
Expand All @@ -270,12 +244,14 @@ class SubscriptionsFragment : Fragment() {
binding.subFeedContainer.isGone = notLoaded
binding.emptyFeed.isVisible = notLoaded

binding.subProgress.visibility = View.GONE
subscriptionsAdapter = VideosAdapter(
sortedFeed.toMutableList(),
showAllAtOnce = false
)
binding.subFeed.adapter = subscriptionsAdapter
if (subscriptionsAdapter == null) {
Log.e("new", "new")
subscriptionsAdapter = VideosAdapter(feed)
binding.subFeed.adapter = subscriptionsAdapter
} else {
val newVideos = feed.subList(subscriptionsAdapter?.itemCount ?: 0, feed.size)
subscriptionsAdapter?.insertItems(newVideos)
}

PreferenceHelper.updateLastFeedWatchedTime()
}
Expand All @@ -293,8 +269,8 @@ class SubscriptionsFragment : Fragment() {
}
}

private fun showSubscriptions() {
if (viewModel.subscriptions.value == null) return
private fun showSubscriptions() = lifecycleScope.launch {
if (viewModel.subscriptions.value == null) return@launch

binding.subRefresh.isRefreshing = false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,33 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class SubscriptionsViewModel : ViewModel() {
var errorResponse = MutableLiveData<Boolean>().apply {
value = false
}

var videoFeed = MutableLiveData<List<StreamItem>?>().apply {
value = null
}
var errorResponse = MutableLiveData<Boolean>()
var videoFeed = MutableLiveData<List<StreamItem>?>()
var subscriptions = MutableLiveData<List<Subscription>?>()
private var loadingSubscriptions: Boolean = false

var subscriptions = MutableLiveData<List<Subscription>?>().apply {
value = null
}
fun fetchFeed(start: Long? = null) {
if (loadingSubscriptions) return
loadingSubscriptions = true

fun fetchFeed() {
// fetching the first / initial page
if (start == null) videoFeed.value = null
viewModelScope.launch(Dispatchers.IO) {
val videoFeed = try {
SubscriptionHelper.getFeed()
SubscriptionHelper.getFeed(start)
} catch (e: Exception) {
errorResponse.postValue(true)
Log.e(TAG(), e.toString())
return@launch
}
[email protected](videoFeed)
if (videoFeed.isNotEmpty()) {
// save the last recent video to the prefs for the notification worker
PreferenceHelper.setLastSeenVideoId(videoFeed[0].url!!.toID())
[email protected](
[email protected]().plus(videoFeed),
)
// save the last recent video to the prefs for the notification worker
if (start == null) videoFeed.firstOrNull()?.let {
PreferenceHelper.setLatestVideoId(it.url!!.toID())
}
loadingSubscriptions = false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
// fetch the users feed
val videoFeed = try {
withContext(Dispatchers.IO) {
SubscriptionHelper.getFeed()
SubscriptionHelper.getFeed(null)
}
} catch (e: Exception) {
return false
Expand Down
Loading