Merge pull request #9747 from Jared234/9126_remove_partially_watched_from_feed

Added option to remove partially watched videos from the 'Whats new' feed
This commit is contained in:
Stypox 2023-02-28 19:10:11 +01:00 committed by GitHub
commit 2e3490bce2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 102 additions and 94 deletions

View File

@ -32,6 +32,7 @@ abstract class FeedDAO {
* @return the feed streams filtered according to the conditions provided in the parameters * @return the feed streams filtered according to the conditions provided in the parameters
* @see StreamStateEntity.isFinished() * @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS * @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @see StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
*/ */
@Query( @Query(
""" """
@ -66,6 +67,15 @@ abstract class FeedDAO {
OR s.stream_type = 'LIVE_STREAM' OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM' OR s.stream_type = 'AUDIO_LIVE_STREAM'
) )
AND (
:includePartiallyPlayed
OR sh.stream_id IS NULL
OR sst.stream_id IS NULL
OR (sst.progress_time <= ${StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS}
AND sst.progress_time <= s.duration * 1000 / 4)
OR (sst.progress_time >= s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
AND sst.progress_time >= s.duration * 1000 * 3 / 4)
)
AND ( AND (
:uploadDateBefore IS NULL :uploadDateBefore IS NULL
OR s.upload_date IS NULL OR s.upload_date IS NULL
@ -79,6 +89,7 @@ abstract class FeedDAO {
abstract fun getStreams( abstract fun getStreams(
groupId: Long, groupId: Long,
includePlayed: Boolean, includePlayed: Boolean,
includePartiallyPlayed: Boolean,
uploadDateBefore: OffsetDateTime? uploadDateBefore: OffsetDateTime?
): Maybe<List<StreamWithState>> ): Maybe<List<StreamWithState>>

View File

@ -30,7 +30,7 @@ public class StreamStateEntity {
/** /**
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s). * Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
*/ */
private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000; public static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
/** /**
* Stream will be considered finished if the playback time left exceeds this threshold * Stream will be considered finished if the playback time left exceeds this threshold

View File

@ -43,11 +43,13 @@ class FeedDatabaseManager(context: Context) {
fun getStreams( fun getStreams(
groupId: Long, groupId: Long,
includePlayedStreams: Boolean, includePlayedStreams: Boolean,
includePartiallyPlayedStreams: Boolean,
includeFutureStreams: Boolean includeFutureStreams: Boolean
): Maybe<List<StreamWithState>> { ): Maybe<List<StreamWithState>> {
return feedTable.getStreams( return feedTable.getStreams(
groupId, groupId,
includePlayedStreams, includePlayedStreams,
includePartiallyPlayedStreams,
if (includeFutureStreams) null else OffsetDateTime.now() if (includeFutureStreams) null else OffsetDateTime.now()
) )
} }

View File

@ -37,11 +37,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -100,8 +98,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private var oldestSubscriptionUpdate: OffsetDateTime? = null private var oldestSubscriptionUpdate: OffsetDateTime? = null
private lateinit var groupAdapter: GroupieAdapter private lateinit var groupAdapter: GroupieAdapter
@State @JvmField var showPlayedItems: Boolean = true
@State @JvmField var showFutureItems: Boolean = true
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
private var updateListViewModeOnResume = false private var updateListViewModeOnResume = false
@ -140,8 +136,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
val factory = FeedViewModel.getFactory(requireContext(), groupId) val factory = FeedViewModel.getFactory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java] viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
showFutureItems = viewModel.getShowFutureItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) } viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
groupAdapter = GroupieAdapter().apply { groupAdapter = GroupieAdapter().apply {
@ -216,8 +210,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
activity.supportActionBar?.subtitle = groupName activity.supportActionBar?.subtitle = groupName
inflater.inflate(R.menu.menu_feed_fragment, menu) inflater.inflate(R.menu.menu_feed_fragment, menu)
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items))
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -243,20 +235,43 @@ class FeedFragment : BaseStateFragment<FeedState>() {
.show() .show()
return true return true
} else if (item.itemId == R.id.menu_item_feed_toggle_played_items) { } else if (item.itemId == R.id.menu_item_feed_toggle_played_items) {
showPlayedItems = !item.isChecked showStreamVisibilityDialog()
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems)
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
} else if (item.itemId == R.id.menu_item_feed_toggle_future_items) {
showFutureItems = !item.isChecked
updateToggleFutureItemsButton(item)
viewModel.toggleFutureItems(showFutureItems)
viewModel.saveShowFutureItemsToPreferences(showFutureItems)
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun showStreamVisibilityDialog() {
val dialogItems = arrayOf(
getString(R.string.feed_show_watched),
getString(R.string.feed_show_partially_watched),
getString(R.string.feed_show_upcoming)
)
val checkedDialogItems = booleanArrayOf(
viewModel.getShowPlayedItemsFromPreferences(),
viewModel.getShowPartiallyPlayedItemsFromPreferences(),
viewModel.getShowFutureItemsFromPreferences()
)
val builder = AlertDialog.Builder(context!!)
builder.setTitle(R.string.feed_hide_streams_title)
builder.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
checkedDialogItems[which] = isChecked
}
builder.setPositiveButton(R.string.ok) { _, _ ->
viewModel.setSaveShowPlayedItems(checkedDialogItems[0])
viewModel.setSaveShowPartiallyPlayedItems(checkedDialogItems[1])
viewModel.setSaveShowFutureItems(checkedDialogItems[2])
}
builder.setNegativeButton(R.string.cancel, null)
builder.create().show()
}
override fun onDestroyOptionsMenu() { override fun onDestroyOptionsMenu() {
super.onDestroyOptionsMenu() super.onDestroyOptionsMenu()
activity?.supportActionBar?.subtitle = null activity?.supportActionBar?.subtitle = null
@ -283,40 +298,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
super.onDestroyView() super.onDestroyView()
} }
private fun updateTogglePlayedItemsButton(menuItem: MenuItem) {
menuItem.isChecked = showPlayedItems
menuItem.icon = AppCompatResources.getDrawable(
requireContext(),
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
)
MenuItemCompat.setTooltipText(
menuItem,
getString(
if (showPlayedItems)
R.string.feed_toggle_hide_played_items
else
R.string.feed_toggle_show_played_items
)
)
}
private fun updateToggleFutureItemsButton(menuItem: MenuItem) {
menuItem.isChecked = showFutureItems
menuItem.icon = AppCompatResources.getDrawable(
requireContext(),
if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history
)
MenuItemCompat.setTooltipText(
menuItem,
getString(
if (showFutureItems)
R.string.feed_toggle_hide_future_items
else
R.string.feed_toggle_show_future_items
)
)
}
// ////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////
// Handling // Handling
// ////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////

View File

@ -11,7 +11,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function5 import io.reactivex.rxjava3.functions.Function6
import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.App import org.schabi.newpipe.App
@ -31,18 +31,24 @@ import java.util.concurrent.TimeUnit
class FeedViewModel( class FeedViewModel(
private val application: Application, private val application: Application,
groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
initialShowPlayedItems: Boolean = true, initialShowPlayedItems: Boolean,
initialShowFutureItems: Boolean = true initialShowPartiallyPlayedItems: Boolean,
initialShowFutureItems: Boolean
) : ViewModel() { ) : ViewModel() {
private val feedDatabaseManager = FeedDatabaseManager(application) private val feedDatabaseManager = FeedDatabaseManager(application)
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>() private val showPlayedItems = BehaviorProcessor.create<Boolean>()
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems private val showPlayedItemsFlowable = showPlayedItems
.startWithItem(initialShowPlayedItems) .startWithItem(initialShowPlayedItems)
.distinctUntilChanged() .distinctUntilChanged()
private val toggleShowFutureItems = BehaviorProcessor.create<Boolean>() private val showPartiallyPlayedItems = BehaviorProcessor.create<Boolean>()
private val toggleShowFutureItemsFlowable = toggleShowFutureItems private val showPartiallyPlayedItemsFlowable = showPartiallyPlayedItems
.startWithItem(initialShowPartiallyPlayedItems)
.distinctUntilChanged()
private val showFutureItems = BehaviorProcessor.create<Boolean>()
private val showFutureItemsFlowable = showFutureItems
.startWithItem(initialShowFutureItems) .startWithItem(initialShowFutureItems)
.distinctUntilChanged() .distinctUntilChanged()
@ -52,23 +58,24 @@ class FeedViewModel(
private var combineDisposable = Flowable private var combineDisposable = Flowable
.combineLatest( .combineLatest(
FeedEventManager.events(), FeedEventManager.events(),
toggleShowPlayedItemsFlowable, showPlayedItemsFlowable,
toggleShowFutureItemsFlowable, showPartiallyPlayedItemsFlowable,
showFutureItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean,
t4: Long, t5: List<OffsetDateTime> -> t5: Long, t6: List<OffsetDateTime> ->
return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull()) return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
} }
) )
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.map { (event, showPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) -> .map { (event, showPlayedItems, showPartiallyPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
val streamItems = if (event is SuccessResultEvent || event is IdleEvent) val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
feedDatabaseManager feedDatabaseManager
.getStreams(groupId, showPlayedItems, showFutureItems) .getStreams(groupId, showPlayedItems, showPartiallyPlayedItems, showFutureItems)
.blockingGet(arrayListOf()) .blockingGet(arrayListOf())
else else
arrayListOf() arrayListOf()
@ -100,8 +107,9 @@ class FeedViewModel(
val t1: FeedEventManager.Event, val t1: FeedEventManager.Event,
val t2: Boolean, val t2: Boolean,
val t3: Boolean, val t3: Boolean,
val t4: Long, val t4: Boolean,
val t5: OffsetDateTime? val t5: Long,
val t6: OffsetDateTime?
) )
private data class CombineResultDataHolder( private data class CombineResultDataHolder(
@ -111,37 +119,49 @@ class FeedViewModel(
val t4: OffsetDateTime? val t4: OffsetDateTime?
) )
fun togglePlayedItems(showPlayedItems: Boolean) { fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
toggleShowPlayedItems.onNext(showPlayedItems) this.showPlayedItems.onNext(showPlayedItems)
}
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
PreferenceManager.getDefaultSharedPreferences(application).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_played_items_key), showPlayedItems) this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
this.apply() this.apply()
} }
}
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(application) fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(application)
fun toggleFutureItems(showFutureItems: Boolean) { fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
toggleShowFutureItems.onNext(showFutureItems) this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
this.apply()
}
} }
fun saveShowFutureItemsToPreferences(showFutureItems: Boolean) = fun getShowPartiallyPlayedItemsFromPreferences() = getShowPartiallyPlayedItemsFromPreferences(application)
fun setSaveShowFutureItems(showFutureItems: Boolean) {
this.showFutureItems.onNext(showFutureItems)
PreferenceManager.getDefaultSharedPreferences(application).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems) this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply() this.apply()
} }
}
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application) fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
companion object { companion object {
private fun getShowPlayedItemsFromPreferences(context: Context) = private fun getShowPlayedItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_played_items_key), true) .getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
private fun getShowPartiallyPlayedItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_partially_watched_items_key), true)
private fun getShowFutureItemsFromPreferences(context: Context) = private fun getShowFutureItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_future_items_key), true) .getBoolean(context.getString(R.string.feed_show_future_items_key), true)
fun getFactory(context: Context, groupId: Long) = viewModelFactory { fun getFactory(context: Context, groupId: Long) = viewModelFactory {
initializer { initializer {
FeedViewModel( FeedViewModel(
@ -149,6 +169,7 @@ class FeedViewModel(
groupId, groupId,
// Read initial value from preferences // Read initial value from preferences
getShowPlayedItemsFromPreferences(context.applicationContext), getShowPlayedItemsFromPreferences(context.applicationContext),
getShowPartiallyPlayedItemsFromPreferences(context.applicationContext),
getShowFutureItemsFromPreferences(context.applicationContext) getShowFutureItemsFromPreferences(context.applicationContext)
) )
} }

View File

@ -87,7 +87,7 @@ public class HistoryRecordManager {
* Marks a stream item as watched such that it is hidden from the feed if watched videos are * Marks a stream item as watched such that it is hidden from the feed if watched videos are
* hidden. Adds a history entry and updates the stream progress to 100%. * hidden. Adds a history entry and updates the stream progress to 100%.
* *
* @see FeedViewModel#togglePlayedItems * @see FeedViewModel#setSaveShowPlayedItems
* @param info the item to mark as watched * @param info the item to mark as watched
* @return a Maybe containing the ID of the item if successful * @return a Maybe containing the ID of the item if successful
*/ */

View File

@ -5,19 +5,8 @@
<item <item
android:id="@+id/menu_item_feed_toggle_played_items" android:id="@+id/menu_item_feed_toggle_played_items"
android:orderInCategory="2" android:orderInCategory="2"
android:checkable="true"
android:checked="true"
android:icon="@drawable/ic_visibility_on" android:icon="@drawable/ic_visibility_on"
android:title="@string/feed_toggle_show_played_items" android:title="@string/feed_show_hide_streams"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_item_feed_toggle_future_items"
android:orderInCategory="3"
android:checkable="true"
android:checked="true"
android:icon="@drawable/ic_history_future"
android:title="@string/feed_toggle_show_future_items"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <item

View File

@ -284,7 +284,8 @@
<string name="feed_update_threshold_key">feed_update_threshold_key</string> <string name="feed_update_threshold_key">feed_update_threshold_key</string>
<string name="feed_update_threshold_default_value">300</string> <string name="feed_update_threshold_default_value">300</string>
<string name="feed_show_played_items_key">feed_show_played_items</string> <string name="feed_show_watched_items_key">feed_show_played_items</string>
<string name="feed_show_partially_watched_items_key">feed_show_partially_watched_items</string>
<string name="feed_show_future_items_key">feed_show_future_items</string> <string name="feed_show_future_items_key">feed_show_future_items</string>
<string name="show_thumbnail_key">show_thumbnail_key</string> <string name="show_thumbnail_key">show_thumbnail_key</string>

View File

@ -694,8 +694,8 @@
\nYouTube is an example of a service that offers this fast method with its RSS feed. \nYouTube is an example of a service that offers this fast method with its RSS feed.
\n \n
\nSo the choice boils down to what you prefer: speed or precise information.</string> \nSo the choice boils down to what you prefer: speed or precise information.</string>
<string name="feed_toggle_show_played_items">Show watched items</string> <string name="feed_hide_streams_title">Show the following streams</string>
<string name="feed_toggle_hide_played_items">Hide watched items</string> <string name="feed_show_hide_streams">Show/Hide streams</string>
<string name="content_not_supported">This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version.</string> <string name="content_not_supported">This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version.</string>
<string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string> <string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string>
<string name="channel_created_by">Created by %s</string> <string name="channel_created_by">Created by %s</string>
@ -763,5 +763,8 @@
<string name="unknown_quality">Unknown quality</string> <string name="unknown_quality">Unknown quality</string>
<string name="feed_toggle_show_future_items">Show future items</string> <string name="feed_toggle_show_future_items">Show future items</string>
<string name="feed_toggle_hide_future_items">Hide future items</string> <string name="feed_toggle_hide_future_items">Hide future items</string>
<string name="feed_show_watched">Fully watched</string>
<string name="feed_show_partially_watched">Partially watched</string>
<string name="feed_show_upcoming">Upcoming</string>
<string name="sort">Sort</string> <string name="sort">Sort</string>
</resources> </resources>