mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-01-11 01:40:59 +00:00
Merge remote-tracking branch 'origin/dev' into notifications-1
This commit is contained in:
commit
892a1df280
@ -106,7 +106,7 @@ ext {
|
||||
androidxWorkVersion = '2.5.0'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.12.3'
|
||||
exoPlayerVersion = '2.14.2'
|
||||
googleAutoServiceVersion = '1.0'
|
||||
groupieVersion = '2.10.0'
|
||||
markwonVersion = '4.6.2'
|
||||
@ -190,7 +190,7 @@ dependencies {
|
||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:4f60225ddc'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:7e7b78f1b3'
|
||||
|
||||
/** Checkstyle **/
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
|
@ -340,8 +340,12 @@
|
||||
<data android:host="skeptikon.fr" />
|
||||
|
||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||
<data android:pathPrefix="/w/p/" /> <!-- short playlist URLs -->
|
||||
<data android:pathPrefix="/accounts/" />
|
||||
<data android:pathPrefix="/a/" /> <!-- short account URLs -->
|
||||
<data android:pathPrefix="/video-channels/" />
|
||||
<data android:pathPrefix="/c/" /> <!-- short video-channels URLs -->
|
||||
</intent-filter>
|
||||
|
||||
<!-- Bandcamp filter for tracks, albums and playlists -->
|
||||
|
@ -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
|
||||
@ -37,7 +38,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
|
||||
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@ -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()
|
||||
@ -97,7 +98,7 @@ abstract class FeedDAO {
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
|
||||
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
@ -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(
|
||||
"""
|
||||
|
@ -378,6 +378,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
|
@ -176,6 +176,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.getStreamType(), context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -65,10 +73,12 @@ 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
|
||||
@ -76,6 +86,7 @@ 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
|
||||
@ -97,6 +108,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private var updateListViewModeOnResume = false
|
||||
private var isRefreshing = false
|
||||
|
||||
private var lastNewItemsCount = 0
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
@ -126,8 +139,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
|
||||
val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems)
|
||||
val factory = FeedViewModel.Factory(requireContext(), groupId)
|
||||
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
||||
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
|
||||
|
||||
groupAdapter = GroupieAdapter().apply {
|
||||
@ -135,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()
|
||||
}
|
||||
@ -158,7 +186,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
}
|
||||
}
|
||||
|
||||
fun setupListViewMode() {
|
||||
private fun setupListViewMode() {
|
||||
// does everything needed to setup the layouts for grid or list modes
|
||||
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
|
||||
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
||||
@ -170,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)
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
@ -213,6 +245,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
showPlayedItems = !item.isChecked
|
||||
updateTogglePlayedItemsButton(item)
|
||||
viewModel.togglePlayedItems(showPlayedItems)
|
||||
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
@ -236,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()
|
||||
@ -355,13 +391,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
val isWatchHistoryEnabled = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean(getString(R.string.enable_watch_history_key), false)
|
||||
if (item.streamType != StreamType.AUDIO_LIVE_STREAM &&
|
||||
item.streamType != StreamType.LIVE_STREAM &&
|
||||
isWatchHistoryEnabled
|
||||
) {
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
)
|
||||
@ -404,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)
|
||||
@ -526,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
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
@ -533,6 +692,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
override fun doInitialLoadLogic() {}
|
||||
|
||||
override fun reloadContent() {
|
||||
hideNewItemsLoaded(false)
|
||||
|
||||
getActivity()?.startService(
|
||||
Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||
|
@ -1,15 +1,18 @@
|
||||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function4
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
@ -23,19 +26,16 @@ import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(
|
||||
applicationContext: Context,
|
||||
private val applicationContext: Context,
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
initialShowPlayedItems: Boolean = true
|
||||
) : ViewModel() {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
||||
|
||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||
private val streamItems = toggleShowPlayedItems
|
||||
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
||||
.startWithItem(initialShowPlayedItems)
|
||||
.distinctUntilChanged()
|
||||
.switchMap { showPlayedItems ->
|
||||
feedDatabaseManager.getStreams(groupId, showPlayedItems)
|
||||
}
|
||||
|
||||
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||
@ -43,17 +43,28 @@ class FeedViewModel(
|
||||
private var combineDisposable = Flowable
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
streamItems,
|
||||
toggleShowPlayedItemsFlowable,
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
|
||||
Function4 { t1: FeedEventManager.Event, t2: Boolean,
|
||||
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)
|
||||
.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())
|
||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||
mutableStateLiveData.postValue(
|
||||
@ -75,20 +86,50 @@ class FeedViewModel(
|
||||
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) {
|
||||
toggleShowPlayedItems.onNext(showPlayedItems)
|
||||
}
|
||||
|
||||
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
|
||||
this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
|
||||
this.apply()
|
||||
}
|
||||
|
||||
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
|
||||
|
||||
companion object {
|
||||
private fun getShowPlayedItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
private val showPlayedItems: Boolean
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
|
||||
return FeedViewModel(
|
||||
context.applicationContext,
|
||||
groupId,
|
||||
// Read initial value from preferences
|
||||
getShowPlayedItemsFromPreferences(context.applicationContext)
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.PicassoHelper
|
||||
import org.schabi.newpipe.util.StreamTypeUtil
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Consumer
|
||||
|
||||
data class StreamItem(
|
||||
val streamWithState: StreamWithState,
|
||||
@ -31,6 +32,12 @@ data class StreamItem(
|
||||
private val stream: StreamEntity = streamWithState.stream
|
||||
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
|
||||
|
||||
enum class ItemVersion { NORMAL, MINI, GRID }
|
||||
@ -97,6 +104,8 @@ data class StreamItem(
|
||||
viewBinding.itemAdditionalDetails.text =
|
||||
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
||||
}
|
||||
|
||||
execBindEnd?.accept(viewBinding)
|
||||
}
|
||||
|
||||
override fun isLongClickable() = when (stream.streamType) {
|
||||
|
@ -120,19 +120,11 @@ public class HistoryRecordManager {
|
||||
}
|
||||
|
||||
// Update the stream progress to the full duration of the video
|
||||
final List<StreamStateEntity> states = streamStateTable.getState(streamId)
|
||||
.blockingFirst();
|
||||
if (!states.isEmpty()) {
|
||||
final StreamStateEntity entity = states.get(0);
|
||||
entity.setProgressMillis(duration * 1000);
|
||||
streamStateTable.update(entity);
|
||||
} else {
|
||||
final StreamStateEntity entity = new StreamStateEntity(
|
||||
streamId,
|
||||
duration * 1000
|
||||
);
|
||||
streamStateTable.insert(entity);
|
||||
}
|
||||
streamStateTable.upsert(entity);
|
||||
|
||||
// Add a history entry
|
||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||
|
@ -366,6 +366,16 @@ public class StatisticsPlaylistFragment
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(
|
||||
item.getStreamEntity().getStreamType(),
|
||||
context
|
||||
)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
@ -782,6 +782,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||
entries.add(StreamDialogEntry.play_with_kodi);
|
||||
}
|
||||
|
||||
// show "mark as watched" only when watch history is enabled
|
||||
if (StreamDialogEntry.shouldAddMarkAsWatched(
|
||||
item.getStreamEntity().getStreamType(),
|
||||
context
|
||||
)) {
|
||||
entries.add(
|
||||
StreamDialogEntry.mark_as_watched
|
||||
);
|
||||
}
|
||||
entries.add(StreamDialogEntry.show_channel_details);
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
@ -1,12 +1,13 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP;
|
||||
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import static com.google.android.exoplayer2.Player.EventListener;
|
||||
import static com.google.android.exoplayer2.Player.Listener;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
@ -116,6 +117,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player.PositionInfo;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
@ -123,13 +125,14 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoListener;
|
||||
import com.google.android.exoplayer2.video.VideoSize;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Target;
|
||||
@ -174,12 +177,12 @@ import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
||||
@ -197,9 +200,8 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
||||
|
||||
public final class Player implements
|
||||
EventListener,
|
||||
PlaybackListener,
|
||||
VideoListener,
|
||||
Listener,
|
||||
SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener,
|
||||
PopupMenu.OnMenuItemClickListener,
|
||||
@ -501,10 +503,6 @@ public final class Player implements
|
||||
|
||||
// Setup video view
|
||||
setupVideoSurface();
|
||||
simpleExoPlayer.addVideoListener(this);
|
||||
|
||||
// Setup subtitle view
|
||||
simpleExoPlayer.addTextOutput(binding.subtitleView);
|
||||
|
||||
// enable media tunneling
|
||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@ -513,7 +511,7 @@ public final class Player implements
|
||||
+ "media tunneling disabled in debug preferences");
|
||||
} else if (DeviceUtils.shouldSupportMediaTunneling()) {
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)));
|
||||
.setTunnelingEnabled(true));
|
||||
} else if (DEBUG) {
|
||||
Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling");
|
||||
}
|
||||
@ -774,6 +772,8 @@ public final class Player implements
|
||||
destroyPlayer();
|
||||
initPlayer(playOnReady);
|
||||
setRepeatMode(repeatMode);
|
||||
// #6825 - Ensure that the shuffle-button is in the correct state on the UI
|
||||
setShuffleButton(binding.shuffleButton, simpleExoPlayer.getShuffleModeEnabled());
|
||||
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
|
||||
|
||||
playQueue = queue;
|
||||
@ -807,7 +807,6 @@ public final class Player implements
|
||||
|
||||
if (!exoPlayerIsNull()) {
|
||||
simpleExoPlayer.removeListener(this);
|
||||
simpleExoPlayer.removeVideoListener(this);
|
||||
simpleExoPlayer.stop();
|
||||
simpleExoPlayer.release();
|
||||
}
|
||||
@ -858,10 +857,10 @@ public final class Player implements
|
||||
|
||||
final int queuePos = playQueue.getIndex();
|
||||
final long windowPos = simpleExoPlayer.getCurrentPosition();
|
||||
final long duration = simpleExoPlayer.getDuration();
|
||||
|
||||
if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) {
|
||||
setRecovery(queuePos, windowPos);
|
||||
}
|
||||
// No checks due to https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380
|
||||
setRecovery(queuePos, Math.max(0, Math.min(windowPos, duration)));
|
||||
}
|
||||
|
||||
private void setRecovery(final int queuePos, final long windowPos) {
|
||||
@ -896,7 +895,7 @@ public final class Player implements
|
||||
|
||||
public void smoothStopPlayer() {
|
||||
// Pausing would make transition from one stream to a new stream not smooth, so only stop
|
||||
simpleExoPlayer.stop(false);
|
||||
simpleExoPlayer.stop();
|
||||
}
|
||||
//endregion
|
||||
|
||||
@ -2435,7 +2434,9 @@ public final class Player implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuityReason) {
|
||||
public void onPositionDiscontinuity(
|
||||
final PositionInfo oldPosition, final PositionInfo newPosition,
|
||||
@DiscontinuityReason final int discontinuityReason) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
||||
+ "discontinuityReason = [" + discontinuityReason + "]");
|
||||
@ -2447,7 +2448,8 @@ public final class Player implements
|
||||
// Refresh the playback if there is a transition to the next video
|
||||
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||
switch (discontinuityReason) {
|
||||
case DISCONTINUITY_REASON_PERIOD_TRANSITION:
|
||||
case DISCONTINUITY_REASON_AUTO_TRANSITION:
|
||||
case DISCONTINUITY_REASON_REMOVE:
|
||||
// When player is in single repeat mode and a period transition occurs,
|
||||
// we need to register a view count here since no metadata has changed
|
||||
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) {
|
||||
@ -2468,7 +2470,7 @@ public final class Player implements
|
||||
playQueue.setIndex(newWindowIndex);
|
||||
}
|
||||
break;
|
||||
case DISCONTINUITY_REASON_AD_INSERTION:
|
||||
case DISCONTINUITY_REASON_SKIP:
|
||||
break; // only makes Android Studio linter happy, as there are no ads
|
||||
}
|
||||
|
||||
@ -2480,6 +2482,11 @@ public final class Player implements
|
||||
//TODO check if this causes black screen when switching to fullscreen
|
||||
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(final List<Cue> cues) {
|
||||
binding.subtitleView.onCues(cues);
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
@ -2501,7 +2508,7 @@ public final class Player implements
|
||||
* </ul>
|
||||
*
|
||||
* @see #processSourceError(IOException)
|
||||
* @see com.google.android.exoplayer2.Player.EventListener#onPlayerError(ExoPlaybackException)
|
||||
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException)
|
||||
*/
|
||||
@Override
|
||||
public void onPlayerError(@NonNull final ExoPlaybackException error) {
|
||||
@ -3865,19 +3872,17 @@ public final class Player implements
|
||||
}
|
||||
|
||||
@Override // exoplayer listener
|
||||
public void onVideoSizeChanged(final int width, final int height,
|
||||
final int unappliedRotationDegrees,
|
||||
final float pixelWidthHeightRatio) {
|
||||
public void onVideoSizeChanged(final VideoSize videoSize) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onVideoSizeChanged() called with: "
|
||||
+ "width / height = [" + width + " / " + height
|
||||
+ " = " + (((float) width) / height) + "], "
|
||||
+ "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], "
|
||||
+ "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
|
||||
+ "width / height = [" + videoSize.width + " / " + videoSize.height
|
||||
+ " = " + (((float) videoSize.width) / videoSize.height) + "], "
|
||||
+ "unappliedRotationDegrees = [" + videoSize.unappliedRotationDegrees + "], "
|
||||
+ "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]");
|
||||
}
|
||||
|
||||
binding.surfaceView.setAspectRatio(((float) width) / height);
|
||||
isVerticalVideo = width < height;
|
||||
binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
|
||||
isVerticalVideo = videoSize.width < videoSize.height;
|
||||
|
||||
if (globalScreenOrientationLocked(context)
|
||||
&& isFullscreen
|
||||
|
@ -16,7 +16,6 @@ import androidx.media.AudioManagerCompat;
|
||||
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
|
||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
||||
|
||||
@ -150,15 +149,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
|
||||
public void onAudioSessionIdChanged(final EventTime eventTime, final int audioSessionId) {
|
||||
notifyAudioSessionUpdate(true, audioSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) {
|
||||
notifyAudioSessionUpdate(false, player.getAudioSessionId());
|
||||
}
|
||||
|
||||
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
|
||||
if (!PlayerHelper.isUsingDSP()) {
|
||||
return;
|
||||
|
@ -1,81 +1,28 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
|
||||
public class LoadController implements LoadControl {
|
||||
public class LoadController extends DefaultLoadControl {
|
||||
|
||||
public static final String TAG = "LoadController";
|
||||
|
||||
private final long initialPlaybackBufferUs;
|
||||
private final LoadControl internalLoadControl;
|
||||
private boolean preloadingEnabled = true;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Default Load Control
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public LoadController() {
|
||||
this(PlayerHelper.getPlaybackStartBufferMs());
|
||||
}
|
||||
|
||||
private LoadController(final int initialPlaybackBufferMs) {
|
||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||
|
||||
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
|
||||
builder.setBufferDurationsMs(
|
||||
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
|
||||
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
|
||||
initialPlaybackBufferMs,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||
internalLoadControl = builder.build();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Custom behaviours
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onPrepared() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onPrepared();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksSelected(final Renderer[] renderers, final TrackGroupArray trackGroups,
|
||||
final TrackSelectionArray trackSelections) {
|
||||
internalLoadControl.onTracksSelected(renderers, trackGroups, trackSelections);
|
||||
super.onPrepared();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onStopped();
|
||||
super.onStopped();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReleased() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onReleased();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Allocator getAllocator() {
|
||||
return internalLoadControl.getAllocator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBackBufferDurationUs() {
|
||||
return internalLoadControl.getBackBufferDurationUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainBackBufferFromKeyframe() {
|
||||
return internalLoadControl.retainBackBufferFromKeyframe();
|
||||
super.onReleased();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -85,20 +32,10 @@ public class LoadController implements LoadControl {
|
||||
if (!preloadingEnabled) {
|
||||
return false;
|
||||
}
|
||||
return internalLoadControl.shouldContinueLoading(
|
||||
return super.shouldContinueLoading(
|
||||
playbackPositionUs, bufferedDurationUs, playbackSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldStartPlayback(final long bufferedDurationUs, final float playbackSpeed,
|
||||
final boolean rebuffering) {
|
||||
final boolean isInitialPlaybackBufferFilled
|
||||
= bufferedDurationUs >= this.initialPlaybackBufferUs * playbackSpeed;
|
||||
final boolean isInternalStartingPlayback = internalLoadControl
|
||||
.shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering);
|
||||
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
|
||||
}
|
||||
|
||||
public void disablePreloadingOfCurrentTrack() {
|
||||
preloadingEnabled = false;
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import static org.schabi.newpipe.player.Player.DEBUG;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
@ -18,9 +21,6 @@ import androidx.preference.PreferenceManager;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.SliderStrategy;
|
||||
|
||||
import static org.schabi.newpipe.player.Player.DEBUG;
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class PlaybackParameterDialog extends DialogFragment {
|
||||
// Minimum allowable range in ExoPlayer
|
||||
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
|
||||
@ -157,7 +157,6 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
setupControlViews(view);
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.playback_speed_control)
|
||||
.setView(view)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
||||
|
@ -1,14 +1,18 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaParserChunkExtractor;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.MediaParserHlsMediaChunkExtractor;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
@ -19,7 +23,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
public class PlayerDataSource {
|
||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
||||
private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||
public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||
|
||||
private final DataSource.Factory cacheDataSourceFactory;
|
||||
private final DataSource.Factory cachelessDataSourceFactory;
|
||||
@ -32,51 +36,83 @@ public class PlayerDataSource {
|
||||
}
|
||||
|
||||
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
|
||||
cachelessDataSourceFactory
|
||||
)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||
final HlsMediaSource.Factory factory =
|
||||
new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||
.setAllowChunklessPreparation(true)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
factory.setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY);
|
||||
}
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(
|
||||
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
|
||||
cachelessDataSourceFactory
|
||||
)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true);
|
||||
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
|
||||
final DataSource.Factory dataSourceFactory
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return new DefaultDashChunkSource.Factory(
|
||||
MediaParserChunkExtractor.FACTORY,
|
||||
dataSourceFactory,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
public SsMediaSource.Factory getSsMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
||||
cacheDataSourceFactory), cacheDataSourceFactory);
|
||||
return new DefaultDashChunkSource.Factory(dataSourceFactory);
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cacheDataSourceFactory);
|
||||
final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(cacheDataSourceFactory);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return factory;
|
||||
}
|
||||
|
||||
// *** >= Android 11 / R / API 30 ***
|
||||
return factory.setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY);
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
||||
cacheDataSourceFactory), cacheDataSourceFactory);
|
||||
return new DashMediaSource.Factory(
|
||||
getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
|
||||
cacheDataSourceFactory
|
||||
);
|
||||
}
|
||||
|
||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
|
||||
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
||||
final ProgressiveMediaSource.Factory factory;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
factory = new ProgressiveMediaSource.Factory(
|
||||
cacheDataSourceFactory,
|
||||
MediaParserExtractorAdapter.FACTORY
|
||||
);
|
||||
} else {
|
||||
factory = new ProgressiveMediaSource.Factory(cacheDataSourceFactory);
|
||||
}
|
||||
|
||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory(
|
||||
@NonNull final String key) {
|
||||
return getExtractorMediaSourceFactory().setCustomCacheKey(key);
|
||||
return factory.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
||||
}
|
||||
|
||||
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
|
||||
|
@ -1,5 +1,18 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
|
||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -21,11 +34,11 @@ import androidx.preference.PreferenceManager;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
|
||||
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@ -57,19 +70,6 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
|
||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
|
||||
public final class PlayerHelper {
|
||||
private static final StringBuilder STRING_BUILDER = new StringBuilder();
|
||||
private static final Formatter STRING_FORMATTER
|
||||
@ -305,14 +305,7 @@ public final class PlayerHelper {
|
||||
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of milliseconds the player buffers for before starting playback
|
||||
*/
|
||||
public static int getPlaybackStartBufferMs() {
|
||||
return 500;
|
||||
}
|
||||
|
||||
public static TrackSelection.Factory getQualitySelector() {
|
||||
public static ExoTrackSelection.Factory getQualitySelector() {
|
||||
return new AdaptiveTrackSelection.Factory(
|
||||
1000,
|
||||
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
|
@ -13,7 +13,7 @@ import com.google.android.exoplayer2.RendererCapabilities.Capabilities;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
@ -28,7 +28,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
private String preferredTextLanguage;
|
||||
|
||||
public CustomTrackSelector(final Context context,
|
||||
final TrackSelection.Factory adaptiveTrackSelectionFactory) {
|
||||
final ExoTrackSelection.Factory adaptiveTrackSelectionFactory) {
|
||||
super(context, adaptiveTrackSelectionFactory);
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Pair<TrackSelection.Definition, TextTrackScore> selectTextTrack(
|
||||
protected Pair<ExoTrackSelection.Definition, TextTrackScore> selectTextTrack(
|
||||
final TrackGroupArray groups,
|
||||
@NonNull final int[][] formatSupport,
|
||||
@NonNull final Parameters params,
|
||||
@ -86,7 +86,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||
}
|
||||
}
|
||||
return selectedGroup == null ? null
|
||||
: Pair.create(new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
|
||||
: Pair.create(new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex),
|
||||
Assertions.checkNotNull(selectedTrackScore));
|
||||
}
|
||||
}
|
||||
|
@ -436,14 +436,16 @@ public abstract class PlayQueue implements Serializable {
|
||||
* top, so shuffling a size-2 list does nothing)
|
||||
*/
|
||||
public synchronized void shuffle() {
|
||||
// Can't shuffle an list that's empty or only has one element
|
||||
if (size() <= 2) {
|
||||
return;
|
||||
}
|
||||
// Create a backup if it doesn't already exist
|
||||
// Note: The backup-list has to be created at all cost (even when size <= 2).
|
||||
// Otherwise it's not possible to enter shuffle-mode!
|
||||
if (backup == null) {
|
||||
backup = new ArrayList<>(streams);
|
||||
}
|
||||
// Can't shuffle a list that's empty or only has one element
|
||||
if (size() <= 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int originalIndex = getIndex();
|
||||
final PlayQueueItem currentItem = getItem();
|
||||
|
@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
@ -41,20 +42,28 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
@NonNull final String sourceUrl,
|
||||
@C.ContentType final int type,
|
||||
@NonNull final MediaSourceTag metadata) {
|
||||
final Uri uri = Uri.parse(sourceUrl);
|
||||
final MediaSourceFactory factory;
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_DASH:
|
||||
return dataSource.getLiveDashMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getLiveDashMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_HLS:
|
||||
return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getLiveHlsMediaSourceFactory();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
|
||||
return factory.createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(sourceUrl))
|
||||
.setLiveTargetOffsetMs(PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -67,21 +76,30 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
|
||||
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
||||
|
||||
final MediaSourceFactory factory;
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_DASH:
|
||||
return dataSource.getDashMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getDashMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_HLS:
|
||||
return dataSource.getHlsMediaSourceFactory().setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getHlsMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_OTHER:
|
||||
return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
factory = dataSource.getExtractorMediaSourceFactory();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
|
||||
return factory.createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(uri)
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.util.TypedValue;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
@ -144,4 +145,11 @@ public final class DeviceUtils {
|
||||
public static boolean isInMultiWindow(final AppCompatActivity activity) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
@ -194,6 +196,16 @@ public enum StreamDialogEntry {
|
||||
void onClick(Fragment fragment, StreamInfoItem infoItem);
|
||||
}
|
||||
|
||||
public static boolean shouldAddMarkAsWatched(final StreamType streamType,
|
||||
final Context context) {
|
||||
final boolean isWatchHistoryEnabled = PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.enable_watch_history_key), false);
|
||||
return streamType != StreamType.AUDIO_LIVE_STREAM
|
||||
&& streamType != StreamType.LIVE_STREAM
|
||||
&& isWatchHistoryEnabled;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// private method to open channel fragment //
|
||||
/////////////////////////////////////////////
|
||||
|
@ -31,22 +31,36 @@
|
||||
android:id="@+id/play_queue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@id/center"
|
||||
android:layout_above="@id/metadata"
|
||||
android:layout_below="@id/appbar"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="LinearLayoutManager"
|
||||
tools:listitem="@layout/play_queue_item" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/center"
|
||||
android:layout_width="match_parent"
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/seek_display"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/playback_controls">
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_above="@id/metadata"
|
||||
android:background="#c0000000"
|
||||
android:paddingLeft="30dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingRight="30dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="1:06:29"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/metadata"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@id/progress_bar"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
@ -81,34 +95,14 @@
|
||||
tools:text="Duis posuere arcu condimentum lobortis mattis." />
|
||||
</LinearLayout>
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/seek_display"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:background="#c0000000"
|
||||
android:paddingLeft="30dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingRight="30dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="1:06:29"
|
||||
tools:visibility="visible" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp">
|
||||
android:paddingRight="12dp"
|
||||
android:layout_above="@+id/playback_controls">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
android:id="@+id/current_time"
|
||||
@ -163,8 +157,9 @@
|
||||
android:id="@+id/playback_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/progress_bar"
|
||||
android:orientation="horizontal"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginBottom="12dp"
|
||||
tools:ignore="RtlHardcoded">
|
||||
|
||||
<ImageButton
|
||||
@ -294,5 +289,4 @@
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -4,9 +4,9 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:paddingLeft="@dimen/video_item_search_padding"
|
||||
android:paddingTop="@dimen/video_item_search_padding"
|
||||
android:paddingRight="@dimen/video_item_search_padding">
|
||||
android:paddingStart="6dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="6dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -31,7 +31,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_below="@id/tempoControlText"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
@ -137,7 +137,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@id/tempoControl"
|
||||
android:layout_margin="@dimen/video_item_search_padding"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:background="?attr/separator_color" />
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
@ -156,7 +159,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_below="@id/pitchControlText"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginTop="3dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.schabi.newpipe.views.NewPipeTextView
|
||||
@ -263,13 +266,16 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@+id/pitchControl"
|
||||
android:layout_margin="@dimen/video_item_search_padding"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:background="?attr/separator_color" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/stepSizeSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_below="@id/separatorStepSizeSelector"
|
||||
android:orientation="horizontal">
|
||||
|
||||
@ -344,32 +350,37 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@+id/stepSizeSelector"
|
||||
android:layout_margin="@dimen/video_item_search_padding"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:background="?attr/separator_color" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/additionalOptions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/separatorCheckbox"
|
||||
android:orientation="vertical">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/unhookCheckbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/separatorCheckbox"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:checked="false"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:maxLines="1"
|
||||
android:text="@string/unhook_checkbox" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/skipSilenceCheckbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/unhookCheckbox"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:checked="false"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:maxLines="1"
|
||||
android:text="@string/skip_silence_checkbox" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- END HERE -->
|
||||
|
||||
|
@ -87,6 +87,19 @@
|
||||
|
||||
</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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -264,6 +264,7 @@
|
||||
|
||||
<string name="feed_update_threshold_key" translatable="false">feed_update_threshold_key</string>
|
||||
<string name="feed_update_threshold_default_value" translatable="false">300</string>
|
||||
<string name="feed_show_played_items_key" translatable="false">feed_show_played_items</string>
|
||||
|
||||
<string name="show_thumbnail_key" translatable="false">show_thumbnail_key</string>
|
||||
|
||||
|
@ -657,6 +657,7 @@
|
||||
<string name="feed_subscription_not_loaded_count">Not loaded: %d</string>
|
||||
<string name="feed_notification_loading">Loading 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_empty_selection">No subscription selected</string>
|
||||
<plurals name="feed_group_dialog_selection_count">
|
||||
|
@ -5,7 +5,6 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
@ -20,7 +19,6 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven { url "https://jitpack.io" }
|
||||
maven { url "https://clojars.org/repo" }
|
||||
}
|
||||
|
1
gradle/wrapper/gradle-wrapper.properties
vendored
1
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip
|
||||
distributionSha256Sum=33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -6,6 +6,6 @@ include ':app'
|
||||
|
||||
//includeBuild('../NewPipeExtractor') {
|
||||
// dependencySubstitution {
|
||||
// substitute module('com.github.TeamNewPipe:NewPipeExtractor') with project(':extractor')
|
||||
// substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor')
|
||||
// }
|
||||
//}
|
||||
|
Loading…
Reference in New Issue
Block a user