Merge pull request #7050 from litetex/feed-refactor-new-items-handling
Rework feed new items handling
This commit is contained in:
commit
d5199eac3e
|
@ -7,6 +7,7 @@ import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
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.FeedEntity
|
||||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||||
import org.schabi.newpipe.database.stream.StreamWithState
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
|
@ -37,7 +38,7 @@ abstract class FeedDAO {
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
|
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
@ -62,7 +63,7 @@ abstract class FeedDAO {
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see StreamStateEntity.isFinished()
|
* @see StreamStateEntity.isFinished()
|
||||||
|
@ -97,7 +98,7 @@ abstract class FeedDAO {
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
|
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see StreamStateEntity.isFinished()
|
* @see StreamStateEntity.isFinished()
|
||||||
|
@ -137,7 +138,7 @@ abstract class FeedDAO {
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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()
|
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
|
||||||
animate().setListener(null).cancel()
|
animate().setListener(null).cancel()
|
||||||
alpha = 0f
|
alpha = 0f
|
||||||
translationY = newTranslationY.toFloat()
|
translationY = newTranslationY.toFloat()
|
||||||
visibility = View.VISIBLE
|
isVisible = true
|
||||||
animate()
|
animate()
|
||||||
.alpha(1f)
|
.alpha(1f)
|
||||||
.translationY(0f)
|
.translationY(0f)
|
||||||
.setStartDelay(delay)
|
.setStartDelay(delay)
|
||||||
.setDuration(duration)
|
.setDuration(duration)
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
execOnEnd?.run()
|
||||||
|
}
|
||||||
|
})
|
||||||
.start()
|
.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ class FeedDatabaseManager(context: Context) {
|
||||||
fun getStreams(
|
fun getStreams(
|
||||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||||
getPlayedStreams: Boolean = true
|
getPlayedStreams: Boolean = true
|
||||||
): Flowable<List<StreamWithState>> {
|
): Maybe<List<StreamWithState>> {
|
||||||
return when (groupId) {
|
return when (groupId) {
|
||||||
FeedGroupEntity.GROUP_ALL_ID -> {
|
FeedGroupEntity.GROUP_ALL_ID -> {
|
||||||
if (getPlayedStreams) feedTable.getAllStreams()
|
if (getPlayedStreams) feedTable.getAllStreams()
|
||||||
|
|
|
@ -21,8 +21,12 @@ package org.schabi.newpipe.local.feed
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
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.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -31,6 +35,8 @@ import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.Nullable
|
import androidx.annotation.Nullable
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
@ -40,8 +46,10 @@ import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.xwray.groupie.GroupieAdapter
|
import com.xwray.groupie.GroupieAdapter
|
||||||
import com.xwray.groupie.Item
|
import com.xwray.groupie.Item
|
||||||
|
import com.xwray.groupie.OnAsyncUpdateListener
|
||||||
import com.xwray.groupie.OnItemClickListener
|
import com.xwray.groupie.OnItemClickListener
|
||||||
import com.xwray.groupie.OnItemLongClickListener
|
import com.xwray.groupie.OnItemLongClickListener
|
||||||
import icepick.State
|
import icepick.State
|
||||||
|
@ -65,10 +73,12 @@ import org.schabi.newpipe.fragments.BaseStateFragment
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog
|
import org.schabi.newpipe.info_list.InfoItemDialog
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
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.item.StreamItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder
|
import org.schabi.newpipe.player.helper.PlayerHolder
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry
|
import org.schabi.newpipe.util.StreamDialogEntry
|
||||||
|
@ -76,6 +86,7 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
||||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
class FeedFragment : BaseStateFragment<FeedState>() {
|
class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
private var _feedBinding: FragmentFeedBinding? = null
|
private var _feedBinding: FragmentFeedBinding? = null
|
||||||
|
@ -97,6 +108,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
private var updateListViewModeOnResume = false
|
private var updateListViewModeOnResume = false
|
||||||
private var isRefreshing = false
|
private var isRefreshing = false
|
||||||
|
|
||||||
|
private var lastNewItemsCount = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
|
@ -136,6 +149,20 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
setOnItemLongClickListener(listenerStreamItem)
|
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
|
feedBinding.itemsList.adapter = groupAdapter
|
||||||
setupListViewMode()
|
setupListViewMode()
|
||||||
}
|
}
|
||||||
|
@ -171,6 +198,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
super.initListeners()
|
super.initListeners()
|
||||||
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
|
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
|
||||||
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
||||||
|
feedBinding.newItemsLoadedButton.setOnClickListener {
|
||||||
|
hideNewItemsLoaded(true)
|
||||||
|
feedBinding.itemsList.scrollToPosition(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -238,6 +269,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
// Ensure that all animations are canceled
|
||||||
|
feedBinding.newItemsLoadedButton?.clearAnimation()
|
||||||
|
|
||||||
feedBinding.itemsList.adapter = null
|
feedBinding.itemsList.adapter = null
|
||||||
_feedBinding = null
|
_feedBinding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
@ -400,7 +434,17 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
}
|
}
|
||||||
loadedState.items.forEach { it.itemVersion = itemVersion }
|
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 {
|
listState?.run {
|
||||||
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
||||||
|
@ -522,6 +566,125 @@ 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
|
// Load Service Handling
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -529,6 +692,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
override fun doInitialLoadLogic() {}
|
override fun doInitialLoadLogic() {}
|
||||||
|
|
||||||
override fun reloadContent() {
|
override fun reloadContent() {
|
||||||
|
hideNewItemsLoaded(false)
|
||||||
|
|
||||||
getActivity()?.startService(
|
getActivity()?.startService(
|
||||||
Intent(requireContext(), FeedLoadService::class.java).apply {
|
Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||||
|
|
|
@ -33,12 +33,9 @@ class FeedViewModel(
|
||||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
||||||
|
|
||||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||||
private val streamItems = toggleShowPlayedItems
|
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
||||||
.startWithItem(initialShowPlayedItems)
|
.startWithItem(initialShowPlayedItems)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.switchMap { showPlayedItems ->
|
|
||||||
feedDatabaseManager.getStreams(groupId, showPlayedItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
||||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||||
|
@ -46,17 +43,28 @@ class FeedViewModel(
|
||||||
private var combineDisposable = Flowable
|
private var combineDisposable = Flowable
|
||||||
.combineLatest(
|
.combineLatest(
|
||||||
FeedEventManager.events(),
|
FeedEventManager.events(),
|
||||||
streamItems,
|
toggleShowPlayedItemsFlowable,
|
||||||
feedDatabaseManager.notLoadedCount(groupId),
|
feedDatabaseManager.notLoadedCount(groupId),
|
||||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||||
|
|
||||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
|
Function4 { t1: FeedEventManager.Event, t2: Boolean,
|
||||||
t3: Long, t4: List<OffsetDateTime> ->
|
t3: Long, t4: List<OffsetDateTime> ->
|
||||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(Schedulers.io())
|
||||||
|
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
|
||||||
|
var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||||
|
feedDatabaseManager
|
||||||
|
.getStreams(groupId, showPlayedItems)
|
||||||
|
.blockingGet(arrayListOf())
|
||||||
|
else
|
||||||
|
arrayListOf()
|
||||||
|
|
||||||
|
CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
|
||||||
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||||
mutableStateLiveData.postValue(
|
mutableStateLiveData.postValue(
|
||||||
|
@ -78,7 +86,19 @@ class FeedViewModel(
|
||||||
combineDisposable.dispose()
|
combineDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamWithState>, val t3: Long, val t4: OffsetDateTime?)
|
private data class CombineResultEventHolder(
|
||||||
|
val t1: FeedEventManager.Event,
|
||||||
|
val t2: Boolean,
|
||||||
|
val t3: Long,
|
||||||
|
val t4: OffsetDateTime?
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class CombineResultDataHolder(
|
||||||
|
val t1: FeedEventManager.Event,
|
||||||
|
val t2: List<StreamWithState>,
|
||||||
|
val t3: Long,
|
||||||
|
val t4: OffsetDateTime?
|
||||||
|
)
|
||||||
|
|
||||||
fun togglePlayedItems(showPlayedItems: Boolean) {
|
fun togglePlayedItems(showPlayedItems: Boolean) {
|
||||||
toggleShowPlayedItems.onNext(showPlayedItems)
|
toggleShowPlayedItems.onNext(showPlayedItems)
|
||||||
|
|
|
@ -19,6 +19,7 @@ import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.PicassoHelper
|
import org.schabi.newpipe.util.PicassoHelper
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
data class StreamItem(
|
data class StreamItem(
|
||||||
val streamWithState: StreamWithState,
|
val streamWithState: StreamWithState,
|
||||||
|
@ -31,6 +32,12 @@ data class StreamItem(
|
||||||
private val stream: StreamEntity = streamWithState.stream
|
private val stream: StreamEntity = streamWithState.stream
|
||||||
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
|
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be executed at the end of the [StreamItem.bind] (with (ListStreamItemBinding,Int)).
|
||||||
|
* Can be used e.g. for highlighting a item.
|
||||||
|
*/
|
||||||
|
var execBindEnd: Consumer<ListStreamItemBinding>? = null
|
||||||
|
|
||||||
override fun getId(): Long = stream.uid
|
override fun getId(): Long = stream.uid
|
||||||
|
|
||||||
enum class ItemVersion { NORMAL, MINI, GRID }
|
enum class ItemVersion { NORMAL, MINI, GRID }
|
||||||
|
@ -97,6 +104,8 @@ data class StreamItem(
|
||||||
viewBinding.itemAdditionalDetails.text =
|
viewBinding.itemAdditionalDetails.text =
|
||||||
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
execBindEnd?.accept(viewBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isLongClickable() = when (stream.streamType) {
|
override fun isLongClickable() = when (stream.streamType) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.BatteryManager;
|
import android.os.BatteryManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
@ -144,4 +145,11 @@ public final class DeviceUtils {
|
||||||
public static boolean isInMultiWindow(final AppCompatActivity activity) {
|
public static boolean isInMultiWindow(final AppCompatActivity activity) {
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hasAnimationsAnimatorDurationEnabled(final Context context) {
|
||||||
|
return Settings.System.getFloat(
|
||||||
|
context.getContentResolver(),
|
||||||
|
Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||||
|
1F) != 0F;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,19 @@
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/new_items_loaded_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignBottom="@id/swipeRefreshLayout"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_marginBottom="5sp"
|
||||||
|
android:text="@string/feed_new_items"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:theme="@style/ServiceColoredButton"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -636,6 +636,7 @@
|
||||||
<string name="feed_subscription_not_loaded_count">Not loaded: %d</string>
|
<string name="feed_subscription_not_loaded_count">Not loaded: %d</string>
|
||||||
<string name="feed_notification_loading">Loading feed…</string>
|
<string name="feed_notification_loading">Loading feed…</string>
|
||||||
<string name="feed_processing_message">Processing feed…</string>
|
<string name="feed_processing_message">Processing feed…</string>
|
||||||
|
<string name="feed_new_items">New feed items</string>
|
||||||
<string name="feed_group_dialog_select_subscriptions">Select subscriptions</string>
|
<string name="feed_group_dialog_select_subscriptions">Select subscriptions</string>
|
||||||
<string name="feed_group_dialog_empty_selection">No subscription selected</string>
|
<string name="feed_group_dialog_empty_selection">No subscription selected</string>
|
||||||
<plurals name="feed_group_dialog_selection_count">
|
<plurals name="feed_group_dialog_selection_count">
|
||||||
|
|
Loading…
Reference in New Issue