Merge remote-tracking branch 'origin/dev' into notifications-1
This commit is contained in:
commit
892a1df280
32 changed files with 602 additions and 321 deletions
|
@ -106,7 +106,7 @@ ext {
|
||||||
androidxWorkVersion = '2.5.0'
|
androidxWorkVersion = '2.5.0'
|
||||||
|
|
||||||
icepickVersion = '3.2.0'
|
icepickVersion = '3.2.0'
|
||||||
exoPlayerVersion = '2.12.3'
|
exoPlayerVersion = '2.14.2'
|
||||||
googleAutoServiceVersion = '1.0'
|
googleAutoServiceVersion = '1.0'
|
||||||
groupieVersion = '2.10.0'
|
groupieVersion = '2.10.0'
|
||||||
markwonVersion = '4.6.2'
|
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
|
// 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/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:4f60225ddc'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:7e7b78f1b3'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||||
|
|
|
@ -340,8 +340,12 @@
|
||||||
<data android:host="skeptikon.fr" />
|
<data android:host="skeptikon.fr" />
|
||||||
|
|
||||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
<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="/accounts/" />
|
||||||
|
<data android:pathPrefix="/a/" /> <!-- short account URLs -->
|
||||||
<data android:pathPrefix="/video-channels/" />
|
<data android:pathPrefix="/video-channels/" />
|
||||||
|
<data android:pathPrefix="/c/" /> <!-- short video-channels URLs -->
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Bandcamp filter for tracks, albums and playlists -->
|
<!-- Bandcamp filter for tracks, albums and playlists -->
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||||
import org.schabi.newpipe.database.stream.StreamWithState
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
|
@ -37,7 +38,7 @@ abstract class FeedDAO {
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
|
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
@ -62,7 +63,7 @@ abstract class FeedDAO {
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see StreamStateEntity.isFinished()
|
* @see StreamStateEntity.isFinished()
|
||||||
|
@ -97,7 +98,7 @@ abstract class FeedDAO {
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
|
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see StreamStateEntity.isFinished()
|
* @see StreamStateEntity.isFinished()
|
||||||
|
@ -137,7 +138,7 @@ abstract class FeedDAO {
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -378,6 +378,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||||
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
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())) {
|
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||||
entries.add(StreamDialogEntry.show_channel_details);
|
entries.add(StreamDialogEntry.show_channel_details);
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,6 +176,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
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())) {
|
if (!isNullOrEmpty(item.getUploaderUrl())) {
|
||||||
entries.add(StreamDialogEntry.show_channel_details);
|
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()
|
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
|
||||||
animate().setListener(null).cancel()
|
animate().setListener(null).cancel()
|
||||||
alpha = 0f
|
alpha = 0f
|
||||||
translationY = newTranslationY.toFloat()
|
translationY = newTranslationY.toFloat()
|
||||||
visibility = View.VISIBLE
|
isVisible = true
|
||||||
animate()
|
animate()
|
||||||
.alpha(1f)
|
.alpha(1f)
|
||||||
.translationY(0f)
|
.translationY(0f)
|
||||||
.setStartDelay(delay)
|
.setStartDelay(delay)
|
||||||
.setDuration(duration)
|
.setDuration(duration)
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
execOnEnd?.run()
|
||||||
|
}
|
||||||
|
})
|
||||||
.start()
|
.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ class FeedDatabaseManager(context: Context) {
|
||||||
fun getStreams(
|
fun getStreams(
|
||||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||||
getPlayedStreams: Boolean = true
|
getPlayedStreams: Boolean = true
|
||||||
): Flowable<List<StreamWithState>> {
|
): Maybe<List<StreamWithState>> {
|
||||||
return when (groupId) {
|
return when (groupId) {
|
||||||
FeedGroupEntity.GROUP_ALL_ID -> {
|
FeedGroupEntity.GROUP_ALL_ID -> {
|
||||||
if (getPlayedStreams) feedTable.getAllStreams()
|
if (getPlayedStreams) feedTable.getAllStreams()
|
||||||
|
|
|
@ -21,8 +21,12 @@ package org.schabi.newpipe.local.feed
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -31,6 +35,8 @@ import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.Nullable
|
import androidx.annotation.Nullable
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
@ -40,8 +46,10 @@ import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.xwray.groupie.GroupieAdapter
|
import com.xwray.groupie.GroupieAdapter
|
||||||
import com.xwray.groupie.Item
|
import com.xwray.groupie.Item
|
||||||
|
import com.xwray.groupie.OnAsyncUpdateListener
|
||||||
import com.xwray.groupie.OnItemClickListener
|
import com.xwray.groupie.OnItemClickListener
|
||||||
import com.xwray.groupie.OnItemLongClickListener
|
import com.xwray.groupie.OnItemLongClickListener
|
||||||
import icepick.State
|
import icepick.State
|
||||||
|
@ -65,10 +73,12 @@ import org.schabi.newpipe.fragments.BaseStateFragment
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog
|
import org.schabi.newpipe.info_list.InfoItemDialog
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
||||||
|
import org.schabi.newpipe.ktx.slideUp
|
||||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder
|
import org.schabi.newpipe.player.helper.PlayerHolder
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.StreamDialogEntry
|
import org.schabi.newpipe.util.StreamDialogEntry
|
||||||
|
@ -76,6 +86,7 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
||||||
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
class FeedFragment : BaseStateFragment<FeedState>() {
|
class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
private var _feedBinding: FragmentFeedBinding? = null
|
private var _feedBinding: FragmentFeedBinding? = null
|
||||||
|
@ -97,6 +108,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
private var updateListViewModeOnResume = false
|
private var updateListViewModeOnResume = false
|
||||||
private var isRefreshing = false
|
private var isRefreshing = false
|
||||||
|
|
||||||
|
private var lastNewItemsCount = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
|
@ -126,8 +139,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||||
super.onViewCreated(rootView, savedInstanceState)
|
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)
|
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
||||||
|
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
||||||
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
|
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
|
||||||
|
|
||||||
groupAdapter = GroupieAdapter().apply {
|
groupAdapter = GroupieAdapter().apply {
|
||||||
|
@ -135,6 +149,20 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
setOnItemLongClickListener(listenerStreamItem)
|
setOnItemLongClickListener(listenerStreamItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feedBinding.itemsList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
// Check if we scrolled to the top
|
||||||
|
if (newState == RecyclerView.SCROLL_STATE_IDLE &&
|
||||||
|
!recyclerView.canScrollVertically(-1)
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (tryGetNewItemsLoadedButton()?.isVisible == true) {
|
||||||
|
hideNewItemsLoaded(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
feedBinding.itemsList.adapter = groupAdapter
|
feedBinding.itemsList.adapter = groupAdapter
|
||||||
setupListViewMode()
|
setupListViewMode()
|
||||||
}
|
}
|
||||||
|
@ -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
|
// does everything needed to setup the layouts for grid or list modes
|
||||||
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
|
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
|
||||||
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
||||||
|
@ -170,6 +198,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
super.initListeners()
|
super.initListeners()
|
||||||
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
|
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
|
||||||
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
||||||
|
feedBinding.newItemsLoadedButton.setOnClickListener {
|
||||||
|
hideNewItemsLoaded(true)
|
||||||
|
feedBinding.itemsList.scrollToPosition(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -213,6 +245,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
showPlayedItems = !item.isChecked
|
showPlayedItems = !item.isChecked
|
||||||
updateTogglePlayedItemsButton(item)
|
updateTogglePlayedItemsButton(item)
|
||||||
viewModel.togglePlayedItems(showPlayedItems)
|
viewModel.togglePlayedItems(showPlayedItems)
|
||||||
|
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
|
@ -236,6 +269,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
// Ensure that all animations are canceled
|
||||||
|
feedBinding.newItemsLoadedButton?.clearAnimation()
|
||||||
|
|
||||||
feedBinding.itemsList.adapter = null
|
feedBinding.itemsList.adapter = null
|
||||||
_feedBinding = null
|
_feedBinding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
@ -355,13 +391,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// show "mark as watched" only when watch history is enabled
|
// show "mark as watched" only when watch history is enabled
|
||||||
val isWatchHistoryEnabled = PreferenceManager
|
if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) {
|
||||||
.getDefaultSharedPreferences(context)
|
|
||||||
.getBoolean(getString(R.string.enable_watch_history_key), false)
|
|
||||||
if (item.streamType != StreamType.AUDIO_LIVE_STREAM &&
|
|
||||||
item.streamType != StreamType.LIVE_STREAM &&
|
|
||||||
isWatchHistoryEnabled
|
|
||||||
) {
|
|
||||||
entries.add(
|
entries.add(
|
||||||
StreamDialogEntry.mark_as_watched
|
StreamDialogEntry.mark_as_watched
|
||||||
)
|
)
|
||||||
|
@ -404,7 +434,17 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
}
|
}
|
||||||
loadedState.items.forEach { it.itemVersion = itemVersion }
|
loadedState.items.forEach { it.itemVersion = itemVersion }
|
||||||
|
|
||||||
groupAdapter.updateAsync(loadedState.items, false, null)
|
// This need to be saved in a variable as the update occurs async
|
||||||
|
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
|
||||||
|
|
||||||
|
groupAdapter.updateAsync(
|
||||||
|
loadedState.items, false,
|
||||||
|
OnAsyncUpdateListener {
|
||||||
|
oldOldestSubscriptionUpdate?.run {
|
||||||
|
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
listState?.run {
|
listState?.run {
|
||||||
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
||||||
|
@ -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
|
// Load Service Handling
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -533,6 +692,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||||
override fun doInitialLoadLogic() {}
|
override fun doInitialLoadLogic() {}
|
||||||
|
|
||||||
override fun reloadContent() {
|
override fun reloadContent() {
|
||||||
|
hideNewItemsLoaded(false)
|
||||||
|
|
||||||
getActivity()?.startService(
|
getActivity()?.startService(
|
||||||
Intent(requireContext(), FeedLoadService::class.java).apply {
|
Intent(requireContext(), FeedLoadService::class.java).apply {
|
||||||
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
package org.schabi.newpipe.local.feed
|
package org.schabi.newpipe.local.feed
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
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.Function4
|
import io.reactivex.rxjava3.functions.Function4
|
||||||
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.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.stream.StreamWithState
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||||
|
@ -23,19 +26,16 @@ import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class FeedViewModel(
|
class FeedViewModel(
|
||||||
applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||||
initialShowPlayedItems: Boolean = true
|
initialShowPlayedItems: Boolean = true
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
||||||
|
|
||||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||||
private val streamItems = toggleShowPlayedItems
|
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
||||||
.startWithItem(initialShowPlayedItems)
|
.startWithItem(initialShowPlayedItems)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.switchMap { showPlayedItems ->
|
|
||||||
feedDatabaseManager.getStreams(groupId, showPlayedItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
||||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||||
|
@ -43,17 +43,28 @@ class FeedViewModel(
|
||||||
private var combineDisposable = Flowable
|
private var combineDisposable = Flowable
|
||||||
.combineLatest(
|
.combineLatest(
|
||||||
FeedEventManager.events(),
|
FeedEventManager.events(),
|
||||||
streamItems,
|
toggleShowPlayedItemsFlowable,
|
||||||
feedDatabaseManager.notLoadedCount(groupId),
|
feedDatabaseManager.notLoadedCount(groupId),
|
||||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||||
|
|
||||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
|
Function4 { t1: FeedEventManager.Event, t2: Boolean,
|
||||||
t3: Long, t4: List<OffsetDateTime> ->
|
t3: Long, t4: List<OffsetDateTime> ->
|
||||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(Schedulers.io())
|
||||||
|
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
|
||||||
|
var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||||
|
feedDatabaseManager
|
||||||
|
.getStreams(groupId, showPlayedItems)
|
||||||
|
.blockingGet(arrayListOf())
|
||||||
|
else
|
||||||
|
arrayListOf()
|
||||||
|
|
||||||
|
CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
|
||||||
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||||
mutableStateLiveData.postValue(
|
mutableStateLiveData.postValue(
|
||||||
|
@ -75,20 +86,50 @@ class FeedViewModel(
|
||||||
combineDisposable.dispose()
|
combineDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamWithState>, val t3: Long, val t4: OffsetDateTime?)
|
private data class CombineResultEventHolder(
|
||||||
|
val t1: FeedEventManager.Event,
|
||||||
|
val t2: Boolean,
|
||||||
|
val t3: Long,
|
||||||
|
val t4: OffsetDateTime?
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class CombineResultDataHolder(
|
||||||
|
val t1: FeedEventManager.Event,
|
||||||
|
val t2: List<StreamWithState>,
|
||||||
|
val t3: Long,
|
||||||
|
val t4: OffsetDateTime?
|
||||||
|
)
|
||||||
|
|
||||||
fun togglePlayedItems(showPlayedItems: Boolean) {
|
fun togglePlayedItems(showPlayedItems: Boolean) {
|
||||||
toggleShowPlayedItems.onNext(showPlayedItems)
|
toggleShowPlayedItems.onNext(showPlayedItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
class Factory(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
|
||||||
private val showPlayedItems: Boolean
|
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
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.PicassoHelper
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
data class StreamItem(
|
data class StreamItem(
|
||||||
val streamWithState: StreamWithState,
|
val streamWithState: StreamWithState,
|
||||||
|
@ -31,6 +32,12 @@ data class StreamItem(
|
||||||
private val stream: StreamEntity = streamWithState.stream
|
private val stream: StreamEntity = streamWithState.stream
|
||||||
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
|
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be executed at the end of the [StreamItem.bind] (with (ListStreamItemBinding,Int)).
|
||||||
|
* Can be used e.g. for highlighting a item.
|
||||||
|
*/
|
||||||
|
var execBindEnd: Consumer<ListStreamItemBinding>? = null
|
||||||
|
|
||||||
override fun getId(): Long = stream.uid
|
override fun getId(): Long = stream.uid
|
||||||
|
|
||||||
enum class ItemVersion { NORMAL, MINI, GRID }
|
enum class ItemVersion { NORMAL, MINI, GRID }
|
||||||
|
@ -97,6 +104,8 @@ data class StreamItem(
|
||||||
viewBinding.itemAdditionalDetails.text =
|
viewBinding.itemAdditionalDetails.text =
|
||||||
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
execBindEnd?.accept(viewBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isLongClickable() = when (stream.streamType) {
|
override fun isLongClickable() = when (stream.streamType) {
|
||||||
|
|
|
@ -120,19 +120,11 @@ public class HistoryRecordManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the stream progress to the full duration of the video
|
// 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(
|
final StreamStateEntity entity = new StreamStateEntity(
|
||||||
streamId,
|
streamId,
|
||||||
duration * 1000
|
duration * 1000
|
||||||
);
|
);
|
||||||
streamStateTable.insert(entity);
|
streamStateTable.upsert(entity);
|
||||||
}
|
|
||||||
|
|
||||||
// Add a history entry
|
// Add a history entry
|
||||||
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||||
|
|
|
@ -366,6 +366,16 @@ public class StatisticsPlaylistFragment
|
||||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
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);
|
entries.add(StreamDialogEntry.show_channel_details);
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries);
|
StreamDialogEntry.setEnabledEntries(entries);
|
||||||
|
|
|
@ -782,6 +782,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||||
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
|
||||||
entries.add(StreamDialogEntry.play_with_kodi);
|
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);
|
entries.add(StreamDialogEntry.show_channel_details);
|
||||||
|
|
||||||
StreamDialogEntry.setEnabledEntries(entries);
|
StreamDialogEntry.setEnabledEntries(entries);
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package org.schabi.newpipe.player;
|
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_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;
|
||||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
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.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_ALL;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
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.DefaultRenderersFactory;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
import com.google.android.exoplayer2.Player.PositionInfo;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
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.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
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.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
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.ui.SubtitleView;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
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.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
import com.squareup.picasso.Target;
|
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.resolver.VideoPlaybackResolver;
|
||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
|
||||||
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PicassoHelper;
|
import org.schabi.newpipe.util.PicassoHelper;
|
||||||
import org.schabi.newpipe.util.SerializedCache;
|
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.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
import org.schabi.newpipe.views.ExpandableSurfaceView;
|
||||||
|
@ -197,9 +200,8 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
||||||
|
|
||||||
public final class Player implements
|
public final class Player implements
|
||||||
EventListener,
|
|
||||||
PlaybackListener,
|
PlaybackListener,
|
||||||
VideoListener,
|
Listener,
|
||||||
SeekBar.OnSeekBarChangeListener,
|
SeekBar.OnSeekBarChangeListener,
|
||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
PopupMenu.OnMenuItemClickListener,
|
PopupMenu.OnMenuItemClickListener,
|
||||||
|
@ -501,10 +503,6 @@ public final class Player implements
|
||||||
|
|
||||||
// Setup video view
|
// Setup video view
|
||||||
setupVideoSurface();
|
setupVideoSurface();
|
||||||
simpleExoPlayer.addVideoListener(this);
|
|
||||||
|
|
||||||
// Setup subtitle view
|
|
||||||
simpleExoPlayer.addTextOutput(binding.subtitleView);
|
|
||||||
|
|
||||||
// enable media tunneling
|
// enable media tunneling
|
||||||
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context)
|
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
@ -513,7 +511,7 @@ public final class Player implements
|
||||||
+ "media tunneling disabled in debug preferences");
|
+ "media tunneling disabled in debug preferences");
|
||||||
} else if (DeviceUtils.shouldSupportMediaTunneling()) {
|
} else if (DeviceUtils.shouldSupportMediaTunneling()) {
|
||||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||||
.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)));
|
.setTunnelingEnabled(true));
|
||||||
} else if (DEBUG) {
|
} else if (DEBUG) {
|
||||||
Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling");
|
Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling");
|
||||||
}
|
}
|
||||||
|
@ -774,6 +772,8 @@ public final class Player implements
|
||||||
destroyPlayer();
|
destroyPlayer();
|
||||||
initPlayer(playOnReady);
|
initPlayer(playOnReady);
|
||||||
setRepeatMode(repeatMode);
|
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);
|
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
|
||||||
|
|
||||||
playQueue = queue;
|
playQueue = queue;
|
||||||
|
@ -807,7 +807,6 @@ public final class Player implements
|
||||||
|
|
||||||
if (!exoPlayerIsNull()) {
|
if (!exoPlayerIsNull()) {
|
||||||
simpleExoPlayer.removeListener(this);
|
simpleExoPlayer.removeListener(this);
|
||||||
simpleExoPlayer.removeVideoListener(this);
|
|
||||||
simpleExoPlayer.stop();
|
simpleExoPlayer.stop();
|
||||||
simpleExoPlayer.release();
|
simpleExoPlayer.release();
|
||||||
}
|
}
|
||||||
|
@ -858,10 +857,10 @@ public final class Player implements
|
||||||
|
|
||||||
final int queuePos = playQueue.getIndex();
|
final int queuePos = playQueue.getIndex();
|
||||||
final long windowPos = simpleExoPlayer.getCurrentPosition();
|
final long windowPos = simpleExoPlayer.getCurrentPosition();
|
||||||
|
final long duration = simpleExoPlayer.getDuration();
|
||||||
|
|
||||||
if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) {
|
// No checks due to https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380
|
||||||
setRecovery(queuePos, windowPos);
|
setRecovery(queuePos, Math.max(0, Math.min(windowPos, duration)));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRecovery(final int queuePos, final long windowPos) {
|
private void setRecovery(final int queuePos, final long windowPos) {
|
||||||
|
@ -896,7 +895,7 @@ public final class Player implements
|
||||||
|
|
||||||
public void smoothStopPlayer() {
|
public void smoothStopPlayer() {
|
||||||
// Pausing would make transition from one stream to a new stream not smooth, so only stop
|
// Pausing would make transition from one stream to a new stream not smooth, so only stop
|
||||||
simpleExoPlayer.stop(false);
|
simpleExoPlayer.stop();
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@ -2435,7 +2434,9 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuityReason) {
|
public void onPositionDiscontinuity(
|
||||||
|
final PositionInfo oldPosition, final PositionInfo newPosition,
|
||||||
|
@DiscontinuityReason final int discontinuityReason) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
|
||||||
+ "discontinuityReason = [" + discontinuityReason + "]");
|
+ "discontinuityReason = [" + discontinuityReason + "]");
|
||||||
|
@ -2447,7 +2448,8 @@ public final class Player implements
|
||||||
// Refresh the playback if there is a transition to the next video
|
// Refresh the playback if there is a transition to the next video
|
||||||
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||||
switch (discontinuityReason) {
|
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,
|
// 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
|
// we need to register a view count here since no metadata has changed
|
||||||
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) {
|
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) {
|
||||||
|
@ -2468,7 +2470,7 @@ public final class Player implements
|
||||||
playQueue.setIndex(newWindowIndex);
|
playQueue.setIndex(newWindowIndex);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DISCONTINUITY_REASON_AD_INSERTION:
|
case DISCONTINUITY_REASON_SKIP:
|
||||||
break; // only makes Android Studio linter happy, as there are no ads
|
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
|
//TODO check if this causes black screen when switching to fullscreen
|
||||||
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
|
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCues(final List<Cue> cues) {
|
||||||
|
binding.subtitleView.onCues(cues);
|
||||||
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
@ -2501,7 +2508,7 @@ public final class Player implements
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @see #processSourceError(IOException)
|
* @see #processSourceError(IOException)
|
||||||
* @see com.google.android.exoplayer2.Player.EventListener#onPlayerError(ExoPlaybackException)
|
* @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException)
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(@NonNull final ExoPlaybackException error) {
|
public void onPlayerError(@NonNull final ExoPlaybackException error) {
|
||||||
|
@ -3865,19 +3872,17 @@ public final class Player implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override // exoplayer listener
|
@Override // exoplayer listener
|
||||||
public void onVideoSizeChanged(final int width, final int height,
|
public void onVideoSizeChanged(final VideoSize videoSize) {
|
||||||
final int unappliedRotationDegrees,
|
|
||||||
final float pixelWidthHeightRatio) {
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "onVideoSizeChanged() called with: "
|
Log.d(TAG, "onVideoSizeChanged() called with: "
|
||||||
+ "width / height = [" + width + " / " + height
|
+ "width / height = [" + videoSize.width + " / " + videoSize.height
|
||||||
+ " = " + (((float) width) / height) + "], "
|
+ " = " + (((float) videoSize.width) / videoSize.height) + "], "
|
||||||
+ "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], "
|
+ "unappliedRotationDegrees = [" + videoSize.unappliedRotationDegrees + "], "
|
||||||
+ "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
|
+ "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.surfaceView.setAspectRatio(((float) width) / height);
|
binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
|
||||||
isVerticalVideo = width < height;
|
isVerticalVideo = videoSize.width < videoSize.height;
|
||||||
|
|
||||||
if (globalScreenOrientationLocked(context)
|
if (globalScreenOrientationLocked(context)
|
||||||
&& isFullscreen
|
&& isFullscreen
|
||||||
|
|
|
@ -16,7 +16,6 @@ import androidx.media.AudioManagerCompat;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
|
||||||
|
|
||||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
|
||||||
|
|
||||||
|
@ -150,15 +149,9 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
|
public void onAudioSessionIdChanged(final EventTime eventTime, final int audioSessionId) {
|
||||||
notifyAudioSessionUpdate(true, 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) {
|
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
|
||||||
if (!PlayerHelper.isUsingDSP()) {
|
if (!PlayerHelper.isUsingDSP()) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,81 +1,28 @@
|
||||||
package org.schabi.newpipe.player.helper;
|
package org.schabi.newpipe.player.helper;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
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";
|
public static final String TAG = "LoadController";
|
||||||
|
|
||||||
private final long initialPlaybackBufferUs;
|
|
||||||
private final LoadControl internalLoadControl;
|
|
||||||
private boolean preloadingEnabled = true;
|
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
|
@Override
|
||||||
public void onPrepared() {
|
public void onPrepared() {
|
||||||
preloadingEnabled = true;
|
preloadingEnabled = true;
|
||||||
internalLoadControl.onPrepared();
|
super.onPrepared();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTracksSelected(final Renderer[] renderers, final TrackGroupArray trackGroups,
|
|
||||||
final TrackSelectionArray trackSelections) {
|
|
||||||
internalLoadControl.onTracksSelected(renderers, trackGroups, trackSelections);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStopped() {
|
public void onStopped() {
|
||||||
preloadingEnabled = true;
|
preloadingEnabled = true;
|
||||||
internalLoadControl.onStopped();
|
super.onStopped();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReleased() {
|
public void onReleased() {
|
||||||
preloadingEnabled = true;
|
preloadingEnabled = true;
|
||||||
internalLoadControl.onReleased();
|
super.onReleased();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Allocator getAllocator() {
|
|
||||||
return internalLoadControl.getAllocator();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBackBufferDurationUs() {
|
|
||||||
return internalLoadControl.getBackBufferDurationUs();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean retainBackBufferFromKeyframe() {
|
|
||||||
return internalLoadControl.retainBackBufferFromKeyframe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -85,20 +32,10 @@ public class LoadController implements LoadControl {
|
||||||
if (!preloadingEnabled) {
|
if (!preloadingEnabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return internalLoadControl.shouldContinueLoading(
|
return super.shouldContinueLoading(
|
||||||
playbackPositionUs, bufferedDurationUs, playbackSpeed);
|
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() {
|
public void disablePreloadingOfCurrentTrack() {
|
||||||
preloadingEnabled = false;
|
preloadingEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package org.schabi.newpipe.player.helper;
|
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.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -18,9 +21,6 @@ import androidx.preference.PreferenceManager;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.util.SliderStrategy;
|
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 {
|
public class PlaybackParameterDialog extends DialogFragment {
|
||||||
// Minimum allowable range in ExoPlayer
|
// Minimum allowable range in ExoPlayer
|
||||||
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
|
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
|
||||||
|
@ -157,7 +157,6 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||||
setupControlViews(view);
|
setupControlViews(view);
|
||||||
|
|
||||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
||||||
.setTitle(R.string.playback_speed_control)
|
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
package org.schabi.newpipe.player.helper;
|
package org.schabi.newpipe.player.helper;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
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.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
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.DefaultSsChunkSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
@ -19,7 +23,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
public class PlayerDataSource {
|
public class PlayerDataSource {
|
||||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
||||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
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 cacheDataSourceFactory;
|
||||||
private final DataSource.Factory cachelessDataSourceFactory;
|
private final DataSource.Factory cachelessDataSourceFactory;
|
||||||
|
@ -32,51 +36,83 @@ public class PlayerDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
||||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
return new SsMediaSource.Factory(
|
||||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
|
||||||
|
cachelessDataSourceFactory
|
||||||
|
)
|
||||||
.setLoadErrorHandlingPolicy(
|
.setLoadErrorHandlingPolicy(
|
||||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
||||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
final HlsMediaSource.Factory factory =
|
||||||
|
new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||||
.setAllowChunklessPreparation(true)
|
.setAllowChunklessPreparation(true)
|
||||||
.setLoadErrorHandlingPolicy(
|
.setLoadErrorHandlingPolicy(
|
||||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
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() {
|
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
|
||||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
final DataSource.Factory dataSourceFactory
|
||||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
) {
|
||||||
.setLoadErrorHandlingPolicy(
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
return new DefaultDashChunkSource.Factory(
|
||||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true);
|
MediaParserChunkExtractor.FACTORY,
|
||||||
|
dataSourceFactory,
|
||||||
|
1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SsMediaSource.Factory getSsMediaSourceFactory() {
|
return new DefaultDashChunkSource.Factory(dataSourceFactory);
|
||||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
|
||||||
cacheDataSourceFactory), cacheDataSourceFactory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
|
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() {
|
public DashMediaSource.Factory getDashMediaSourceFactory() {
|
||||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
return new DashMediaSource.Factory(
|
||||||
cacheDataSourceFactory), cacheDataSourceFactory);
|
getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
|
||||||
|
cacheDataSourceFactory
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
|
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
|
||||||
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
|
final ProgressiveMediaSource.Factory factory;
|
||||||
.setLoadErrorHandlingPolicy(
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
factory = new ProgressiveMediaSource.Factory(
|
||||||
|
cacheDataSourceFactory,
|
||||||
|
MediaParserExtractorAdapter.FACTORY
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
factory = new ProgressiveMediaSource.Factory(cacheDataSourceFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory(
|
return factory.setLoadErrorHandlingPolicy(
|
||||||
@NonNull final String key) {
|
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
||||||
return getExtractorMediaSourceFactory().setCustomCacheKey(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
|
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
package org.schabi.newpipe.player.helper;
|
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.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
@ -21,11 +34,11 @@ import androidx.preference.PreferenceManager;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
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.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;
|
||||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
|
||||||
|
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -57,19 +70,6 @@ import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
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 {
|
public final class PlayerHelper {
|
||||||
private static final StringBuilder STRING_BUILDER = new StringBuilder();
|
private static final StringBuilder STRING_BUILDER = new StringBuilder();
|
||||||
private static final Formatter STRING_FORMATTER
|
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 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static ExoTrackSelection.Factory getQualitySelector() {
|
||||||
* @return the number of milliseconds the player buffers for before starting playback
|
|
||||||
*/
|
|
||||||
public static int getPlaybackStartBufferMs() {
|
|
||||||
return 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TrackSelection.Factory getQualitySelector() {
|
|
||||||
return new AdaptiveTrackSelection.Factory(
|
return new AdaptiveTrackSelection.Factory(
|
||||||
1000,
|
1000,
|
||||||
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
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.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
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;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,7 +28,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||||
private String preferredTextLanguage;
|
private String preferredTextLanguage;
|
||||||
|
|
||||||
public CustomTrackSelector(final Context context,
|
public CustomTrackSelector(final Context context,
|
||||||
final TrackSelection.Factory adaptiveTrackSelectionFactory) {
|
final ExoTrackSelection.Factory adaptiveTrackSelectionFactory) {
|
||||||
super(context, adaptiveTrackSelectionFactory);
|
super(context, adaptiveTrackSelectionFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
protected Pair<TrackSelection.Definition, TextTrackScore> selectTextTrack(
|
protected Pair<ExoTrackSelection.Definition, TextTrackScore> selectTextTrack(
|
||||||
final TrackGroupArray groups,
|
final TrackGroupArray groups,
|
||||||
@NonNull final int[][] formatSupport,
|
@NonNull final int[][] formatSupport,
|
||||||
@NonNull final Parameters params,
|
@NonNull final Parameters params,
|
||||||
|
@ -86,7 +86,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return selectedGroup == null ? null
|
return selectedGroup == null ? null
|
||||||
: Pair.create(new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
|
: Pair.create(new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex),
|
||||||
Assertions.checkNotNull(selectedTrackScore));
|
Assertions.checkNotNull(selectedTrackScore));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -436,14 +436,16 @@ public abstract class PlayQueue implements Serializable {
|
||||||
* top, so shuffling a size-2 list does nothing)
|
* top, so shuffling a size-2 list does nothing)
|
||||||
*/
|
*/
|
||||||
public synchronized void shuffle() {
|
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
|
// 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) {
|
if (backup == null) {
|
||||||
backup = new ArrayList<>(streams);
|
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 int originalIndex = getIndex();
|
||||||
final PlayQueueItem currentItem = getItem();
|
final PlayQueueItem currentItem = getItem();
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
@ -41,20 +42,28 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
@NonNull final String sourceUrl,
|
@NonNull final String sourceUrl,
|
||||||
@C.ContentType final int type,
|
@C.ContentType final int type,
|
||||||
@NonNull final MediaSourceTag metadata) {
|
@NonNull final MediaSourceTag metadata) {
|
||||||
final Uri uri = Uri.parse(sourceUrl);
|
final MediaSourceFactory factory;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
|
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||||
.createMediaSource(MediaItem.fromUri(uri));
|
break;
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return dataSource.getLiveDashMediaSourceFactory().setTag(metadata)
|
factory = dataSource.getLiveDashMediaSourceFactory();
|
||||||
.createMediaSource(MediaItem.fromUri(uri));
|
break;
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata)
|
factory = dataSource.getLiveHlsMediaSourceFactory();
|
||||||
.createMediaSource(MediaItem.fromUri(uri));
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
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
|
@NonNull
|
||||||
|
@ -67,21 +76,30 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||||
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
|
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
|
||||||
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
||||||
|
|
||||||
|
final MediaSourceFactory factory;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
|
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||||
.createMediaSource(MediaItem.fromUri(uri));
|
break;
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return dataSource.getDashMediaSourceFactory().setTag(metadata)
|
factory = dataSource.getDashMediaSourceFactory();
|
||||||
.createMediaSource(MediaItem.fromUri(uri));
|
break;
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return dataSource.getHlsMediaSourceFactory().setTag(metadata)
|
factory = dataSource.getHlsMediaSourceFactory();
|
||||||
.createMediaSource(MediaItem.fromUri(uri));
|
break;
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata)
|
factory = dataSource.getExtractorMediaSourceFactory();
|
||||||
.createMediaSource(MediaItem.fromUri(uri));
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
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.content.res.Configuration;
|
||||||
import android.os.BatteryManager;
|
import android.os.BatteryManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
@ -144,4 +145,11 @@ public final class DeviceUtils {
|
||||||
public static boolean isInMultiWindow(final AppCompatActivity activity) {
|
public static boolean isInMultiWindow(final AppCompatActivity activity) {
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hasAnimationsAnimatorDurationEnabled(final Context context) {
|
||||||
|
return Settings.System.getFloat(
|
||||||
|
context.getContentResolver(),
|
||||||
|
Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||||
|
1F) != 0F;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,13 @@ import android.net.Uri;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
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.PlaylistAppendDialog;
|
||||||
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
@ -194,6 +196,16 @@ public enum StreamDialogEntry {
|
||||||
void onClick(Fragment fragment, StreamInfoItem infoItem);
|
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 //
|
// private method to open channel fragment //
|
||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
|
|
|
@ -31,22 +31,36 @@
|
||||||
android:id="@+id/play_queue"
|
android:id="@+id/play_queue"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_above="@id/center"
|
android:layout_above="@id/metadata"
|
||||||
android:layout_below="@id/appbar"
|
android:layout_below="@id/appbar"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="LinearLayoutManager"
|
app:layoutManager="LinearLayoutManager"
|
||||||
tools:listitem="@layout/play_queue_item" />
|
tools:listitem="@layout/play_queue_item" />
|
||||||
|
|
||||||
<RelativeLayout
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/center"
|
android:id="@+id/seek_display"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="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
|
<LinearLayout
|
||||||
android:id="@+id/metadata"
|
android:id="@+id/metadata"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_above="@id/progress_bar"
|
||||||
android:background="?attr/selectableItemBackground"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
|
@ -81,34 +95,14 @@
|
||||||
tools:text="Duis posuere arcu condimentum lobortis mattis." />
|
tools:text="Duis posuere arcu condimentum lobortis mattis." />
|
||||||
</LinearLayout>
|
</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
|
<LinearLayout
|
||||||
android:id="@+id/progress_bar"
|
android:id="@+id/progress_bar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingLeft="12dp"
|
android:paddingLeft="12dp"
|
||||||
android:paddingRight="12dp">
|
android:paddingRight="12dp"
|
||||||
|
android:layout_above="@+id/playback_controls">
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/current_time"
|
android:id="@+id/current_time"
|
||||||
|
@ -163,8 +157,9 @@
|
||||||
android:id="@+id/playback_controls"
|
android:id="@+id/playback_controls"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_above="@+id/progress_bar"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
tools:ignore="RtlHardcoded">
|
tools:ignore="RtlHardcoded">
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
@ -294,5 +289,4 @@
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clickable="false"
|
android:clickable="false"
|
||||||
android:paddingLeft="@dimen/video_item_search_padding"
|
android:paddingStart="6dp"
|
||||||
android:paddingTop="@dimen/video_item_search_padding"
|
android:paddingTop="4dp"
|
||||||
android:paddingRight="@dimen/video_item_search_padding">
|
android:paddingEnd="6dp">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_below="@id/tempoControlText"
|
android:layout_below="@id/tempoControlText"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="3dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
@ -137,7 +137,10 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_below="@id/tempoControl"
|
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" />
|
android:background="?attr/separator_color" />
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
@ -156,7 +159,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_below="@id/pitchControlText"
|
android:layout_below="@id/pitchControlText"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="3dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
|
@ -263,13 +266,16 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_below="@+id/pitchControl"
|
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" />
|
android:background="?attr/separator_color" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/stepSizeSelector"
|
android:id="@+id/stepSizeSelector"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="40dp"
|
android:layout_height="32dp"
|
||||||
android:layout_below="@id/separatorStepSizeSelector"
|
android:layout_below="@id/separatorStepSizeSelector"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
@ -344,32 +350,37 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_below="@+id/stepSizeSelector"
|
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" />
|
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
|
<CheckBox
|
||||||
android:id="@+id/unhookCheckbox"
|
android:id="@+id/unhookCheckbox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/separatorCheckbox"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:checked="false"
|
android:checked="false"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:maxLines="1"
|
|
||||||
android:text="@string/unhook_checkbox" />
|
android:text="@string/unhook_checkbox" />
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/skipSilenceCheckbox"
|
android:id="@+id/skipSilenceCheckbox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/unhookCheckbox"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:checked="false"
|
android:checked="false"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:maxLines="1"
|
|
||||||
android:text="@string/skip_silence_checkbox" />
|
android:text="@string/skip_silence_checkbox" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- END HERE -->
|
<!-- END HERE -->
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,19 @@
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/new_items_loaded_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignBottom="@id/swipeRefreshLayout"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_marginBottom="5sp"
|
||||||
|
android:text="@string/feed_new_items"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:theme="@style/ServiceColoredButton"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -264,6 +264,7 @@
|
||||||
|
|
||||||
<string name="feed_update_threshold_key" translatable="false">feed_update_threshold_key</string>
|
<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_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>
|
<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_subscription_not_loaded_count">Not loaded: %d</string>
|
||||||
<string name="feed_notification_loading">Loading feed…</string>
|
<string name="feed_notification_loading">Loading feed…</string>
|
||||||
<string name="feed_processing_message">Processing feed…</string>
|
<string name="feed_processing_message">Processing feed…</string>
|
||||||
|
<string name="feed_new_items">New feed items</string>
|
||||||
<string name="feed_group_dialog_select_subscriptions">Select subscriptions</string>
|
<string name="feed_group_dialog_select_subscriptions">Select subscriptions</string>
|
||||||
<string name="feed_group_dialog_empty_selection">No subscription selected</string>
|
<string name="feed_group_dialog_empty_selection">No subscription selected</string>
|
||||||
<plurals name="feed_group_dialog_selection_count">
|
<plurals name="feed_group_dialog_selection_count">
|
||||||
|
|
|
@ -5,7 +5,6 @@ buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||||
|
@ -20,7 +19,6 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
maven { url "https://clojars.org/repo" }
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip
|
||||||
|
distributionSha256Sum=33ad4583fd7ee156f533778736fa1b4940bd83b433934d1cc4e9f608e99a6a89
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -6,6 +6,6 @@ include ':app'
|
||||||
|
|
||||||
//includeBuild('../NewPipeExtractor') {
|
//includeBuild('../NewPipeExtractor') {
|
||||||
// dependencySubstitution {
|
// dependencySubstitution {
|
||||||
// substitute module('com.github.TeamNewPipe:NewPipeExtractor') with project(':extractor')
|
// substitute module('com.github.TeamNewPipe:NewPipeExtractor') using project(':extractor')
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
Loading…
Reference in a new issue