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

Rework feed new items handling #7050

Merged
merged 13 commits into from
Nov 15, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState
Expand Down Expand Up @@ -37,7 +38,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
abstract fun getAllStreams(): Maybe<List<StreamWithState>>

@Query(
"""
Expand All @@ -62,7 +63,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>

/**
* @see StreamStateEntity.isFinished()
Expand Down Expand Up @@ -97,7 +98,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>

/**
* @see StreamStateEntity.isFinished()
Expand Down Expand Up @@ -137,7 +138,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>

@Query(
"""
Expand Down
22 changes: 20 additions & 2 deletions app/src/main/java/org/schabi/newpipe/ktx/View.kt
Original file line number Diff line number Diff line change
Expand Up @@ -299,18 +299,36 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
}
}

fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float) {
fun View.slideUp(
duration: Long,
delay: Long,
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float
) {
slideUp(duration, delay, translationPercent, null)
}

fun View.slideUp(
duration: Long,
delay: Long = 0L,
@FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F,
execOnEnd: Runnable? = null
) {
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
animate().setListener(null).cancel()
alpha = 0f
translationY = newTranslationY.toFloat()
visibility = View.VISIBLE
isVisible = true
animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
})
.start()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class FeedDatabaseManager(context: Context) {
fun getStreams(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
getPlayedStreams: Boolean = true
): Flowable<List<StreamWithState>> {
): Maybe<List<StreamWithState>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> {
if (getPlayedStreams) feedTable.getAllStreams()
Expand Down
167 changes: 166 additions & 1 deletion app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ package org.schabi.newpipe.local.feed

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
Expand All @@ -31,6 +35,8 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.annotation.AttrRes
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
Expand All @@ -40,8 +46,10 @@ import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Item
import com.xwray.groupie.OnAsyncUpdateListener
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
Expand All @@ -65,17 +73,20 @@ import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.info_list.InfoItemDialog
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.ktx.slideUp
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.player.helper.PlayerHolder
import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.StreamDialogEntry
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.ArrayList
import java.util.function.Consumer

class FeedFragment : BaseStateFragment<FeedState>() {
private var _feedBinding: FragmentFeedBinding? = null
Expand All @@ -97,6 +108,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private var updateListViewModeOnResume = false
private var isRefreshing = false

private var lastNewItemsCount = 0

init {
setHasOptionsMenu(true)
}
Expand Down Expand Up @@ -136,6 +149,20 @@ class FeedFragment : BaseStateFragment<FeedState>() {
setOnItemLongClickListener(listenerStreamItem)
}

feedBinding.itemsList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
// Check if we scrolled to the top
if (newState == RecyclerView.SCROLL_STATE_IDLE &&
!recyclerView.canScrollVertically(-1)
) {

if (tryGetNewItemsLoadedButton()?.isVisible == true) {
hideNewItemsLoaded(true)
}
}
}
})

feedBinding.itemsList.adapter = groupAdapter
setupListViewMode()
}
Expand Down Expand Up @@ -171,6 +198,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
super.initListeners()
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
feedBinding.newItemsLoadedButton.setOnClickListener {
hideNewItemsLoaded(true)
feedBinding.itemsList.scrollToPosition(0)
}
}

// /////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -238,6 +269,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
}

override fun onDestroyView() {
// Ensure that all animations are canceled
feedBinding.newItemsLoadedButton?.clearAnimation()

feedBinding.itemsList.adapter = null
_feedBinding = null
super.onDestroyView()
Expand Down Expand Up @@ -400,7 +434,17 @@ class FeedFragment : BaseStateFragment<FeedState>() {
}
loadedState.items.forEach { it.itemVersion = itemVersion }

groupAdapter.updateAsync(loadedState.items, false, null)
// This need to be saved in a variable as the update occurs async
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate

groupAdapter.updateAsync(
loadedState.items, false,
OnAsyncUpdateListener {
oldOldestSubscriptionUpdate?.run {
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
}
}
)

listState?.run {
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
Expand Down Expand Up @@ -522,13 +566,134 @@ class FeedFragment : BaseStateFragment<FeedState>() {
)
}

/**
* Highlights all items that are after the specified time
*/
private fun highlightNewItemsAfter(updateTime: OffsetDateTime) {
var highlightCount = 0

var doCheck = true

for (i in 0 until groupAdapter.itemCount) {
val item = groupAdapter.getItem(i) as StreamItem

var typeface = Typeface.DEFAULT
var backgroundSupplier = { ctx: Context ->
resolveDrawable(ctx, R.attr.selectableItemBackground)
}
if (doCheck) {
// If the uploadDate is null or true we should highlight the item
if (item.streamWithState.stream.uploadDate?.isAfter(updateTime) != false) {
highlightCount++

typeface = Typeface.DEFAULT_BOLD
backgroundSupplier = { ctx: Context ->
// Merge the drawables together. Otherwise we would lose the "select" effect
LayerDrawable(
arrayOf(
resolveDrawable(ctx, R.attr.dashed_border),
resolveDrawable(ctx, R.attr.selectableItemBackground)
)
)
}
} else {
// Decreases execution time due to the order of the items (newest always on top)
// Once a item is is before the updateTime we can skip all following items
doCheck = false
}
}

// The highlighter has to be always set
// When it's only set on items that are highlighted it will highlight all items
// due to the fact that itemRoot is getting recycled
item.execBindEnd = Consumer { viewBinding ->
val context = viewBinding.itemRoot.context
viewBinding.itemRoot.background = backgroundSupplier.invoke(context)
viewBinding.itemVideoTitleView.typeface = typeface
}
}

// Force updates all items so that the highlighting is correct
// If this isn't done visible items that are already highlighted will stay in a highlighted
// state until the user scrolls them out of the visible area which causes a update/bind-call
groupAdapter.notifyItemRangeChanged(
0,
minOf(groupAdapter.itemCount, maxOf(highlightCount, lastNewItemsCount))
)

if (highlightCount > 0) {
showNewItemsLoaded()
}

lastNewItemsCount = highlightCount
}

private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
return androidx.core.content.ContextCompat.getDrawable(
context,
android.util.TypedValue().apply {
context.theme.resolveAttribute(
attrResId,
this,
true
)
}.resourceId
)
}

private fun showNewItemsLoaded() {
tryGetNewItemsLoadedButton()?.clearAnimation()
tryGetNewItemsLoadedButton()
?.slideUp(
250L,
delay = 100,
execOnEnd = {
// Disabled animations would result in immediately hiding the button
// after it showed up
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
// Hide the new items-"popup" after 10s
hideNewItemsLoaded(true, 10000)
}
}
)
}

private fun hideNewItemsLoaded(animate: Boolean, delay: Long = 0) {
tryGetNewItemsLoadedButton()?.clearAnimation()
if (animate) {
tryGetNewItemsLoadedButton()?.animate(
false,
200,
delay = delay,
execOnEnd = {
// Make the layout invisible so that the onScroll toTop method
// only does necessary work
tryGetNewItemsLoadedButton()?.isVisible = false
}
)
} else {
tryGetNewItemsLoadedButton()?.isVisible = false
}
}

/**
* The view/button can be disposed/set to null under certain circumstances.
* E.g. when the animation is still in progress but the view got destroyed.
* This method is a helper for such states and can be used in affected code blocks.
*/
private fun tryGetNewItemsLoadedButton(): Button? {
return _feedBinding?.newItemsLoadedButton
}

// /////////////////////////////////////////////////////////////////////////
// Load Service Handling
// /////////////////////////////////////////////////////////////////////////

override fun doInitialLoadLogic() {}

override fun reloadContent() {
hideNewItemsLoaded(false)

getActivity()?.startService(
Intent(requireContext(), FeedLoadService::class.java).apply {
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
Expand Down
Loading