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)
|
final StreamStateEntity entity = new StreamStateEntity(
|
||||||
.blockingFirst();
|
streamId,
|
||||||
if (!states.isEmpty()) {
|
duration * 1000
|
||||||
final StreamStateEntity entity = states.get(0);
|
);
|
||||||
entity.setProgressMillis(duration * 1000);
|
streamStateTable.upsert(entity);
|
||||||
streamStateTable.update(entity);
|
|
||||||
} else {
|
|
||||||
final StreamStateEntity entity = new StreamStateEntity(
|
|
||||||
streamId,
|
|
||||||
duration * 1000
|
|
||||||
);
|
|
||||||
streamStateTable.insert(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 =
|
||||||
.setAllowChunklessPreparation(true)
|
new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||||
|
.setAllowChunklessPreparation(true)
|
||||||
|
.setLoadErrorHandlingPolicy(
|
||||||
|
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
factory.setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||||
|
return new DashMediaSource.Factory(
|
||||||
|
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
|
||||||
|
cachelessDataSourceFactory
|
||||||
|
)
|
||||||
.setLoadErrorHandlingPolicy(
|
.setLoadErrorHandlingPolicy(
|
||||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
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,84 +31,78 @@
|
||||||
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="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_above="@id/metadata"
|
||||||
|
android:background="#c0000000"
|
||||||
|
android:paddingLeft="30dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingRight="30dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="1:06:29"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/metadata"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_above="@+id/playback_controls">
|
android:layout_above="@id/progress_bar"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
<LinearLayout
|
android:clickable="true"
|
||||||
android:id="@+id/metadata"
|
android:focusable="true"
|
||||||
android:layout_width="match_parent"
|
android:orientation="vertical"
|
||||||
android:layout_height="wrap_content"
|
android:padding="8dp"
|
||||||
android:background="?attr/selectableItemBackground"
|
tools:ignore="RtlHardcoded,RtlSymmetry">
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="8dp"
|
|
||||||
tools:ignore="RtlHardcoded,RtlSymmetry">
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/song_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="marquee"
|
|
||||||
android:fadingEdge="horizontal"
|
|
||||||
android:marqueeRepeatLimit="marquee_forever"
|
|
||||||
android:scrollHorizontally="true"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
||||||
android:textSize="14sp"
|
|
||||||
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec aliquam augue, eget cursus est. Ut id tristique enim, ut scelerisque tellus. Sed ultricies ipsum non mauris ultricies, commodo malesuada velit porta." />
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
|
||||||
android:id="@+id/artist_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="marquee"
|
|
||||||
android:fadingEdge="horizontal"
|
|
||||||
android:marqueeRepeatLimit="marquee_forever"
|
|
||||||
android:scrollHorizontally="true"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
|
||||||
android:textSize="12sp"
|
|
||||||
tools:text="Duis posuere arcu condimentum lobortis mattis." />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<org.schabi.newpipe.views.NewPipeTextView
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
android:id="@+id/seek_display"
|
android:id="@+id/song_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:ellipsize="marquee"
|
||||||
android:background="#c0000000"
|
android:fadingEdge="horizontal"
|
||||||
android:paddingLeft="30dp"
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
android:paddingTop="5dp"
|
android:scrollHorizontally="true"
|
||||||
android:paddingRight="30dp"
|
android:singleLine="true"
|
||||||
android:paddingBottom="5dp"
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
android:textColor="@android:color/white"
|
android:textSize="14sp"
|
||||||
android:textSize="22sp"
|
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec aliquam augue, eget cursus est. Ut id tristique enim, ut scelerisque tellus. Sed ultricies ipsum non mauris ultricies, commodo malesuada velit porta." />
|
||||||
android:textStyle="bold"
|
|
||||||
android:visibility="gone"
|
<org.schabi.newpipe.views.NewPipeTextView
|
||||||
tools:ignore="RtlHardcoded"
|
android:id="@+id/artist_name"
|
||||||
tools:text="1:06:29"
|
android:layout_width="match_parent"
|
||||||
tools:visibility="visible" />
|
android:layout_height="wrap_content"
|
||||||
</RelativeLayout>
|
android:ellipsize="marquee"
|
||||||
|
android:fadingEdge="horizontal"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textSize="12sp"
|
||||||
|
tools:text="Duis posuere arcu condimentum lobortis mattis." />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<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" />
|
||||||
|
|
||||||
<CheckBox
|
<LinearLayout
|
||||||
android:id="@+id/unhookCheckbox"
|
android:id="@+id/additionalOptions"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/separatorCheckbox"
|
android:layout_below="@id/separatorCheckbox"
|
||||||
android:layout_centerHorizontal="true"
|
android:orientation="vertical">
|
||||||
android:checked="false"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:text="@string/unhook_checkbox" />
|
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/skipSilenceCheckbox"
|
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/unhookCheckbox"
|
android:checked="false"
|
||||||
android:layout_centerHorizontal="true"
|
android:clickable="true"
|
||||||
android:checked="false"
|
android:focusable="true"
|
||||||
android:clickable="true"
|
android:text="@string/unhook_checkbox" />
|
||||||
android:focusable="true"
|
|
||||||
android:maxLines="1"
|
<CheckBox
|
||||||
android:text="@string/skip_silence_checkbox" />
|
android:id="@+id/skipSilenceCheckbox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="false"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
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