1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-09-09 14:26:00 +00:00

More improve Kotlin converted from java in various places

This commit is contained in:
Stypox
2025-06-10 18:30:56 +02:00
parent 317db719db
commit a4182b474b
2 changed files with 122 additions and 164 deletions

View File

@@ -7,7 +7,6 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.database.ContentObserver import android.database.ContentObserver
@@ -18,7 +17,6 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.text.TextUtils
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
@@ -44,14 +42,14 @@ import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import coil3.util.CoilUtils.dispose import coil3.util.CoilUtils
import com.evernote.android.state.State import com.evernote.android.state.State
import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.PlaybackParameters import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@@ -115,17 +113,15 @@ import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
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.util.image.CoilHelper.loadAvatar import org.schabi.newpipe.util.image.CoilHelper
import org.schabi.newpipe.util.image.CoilHelper.loadDetailsThumbnail
import java.util.LinkedList import java.util.LinkedList
import java.util.Objects
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class VideoDetailFragment : class VideoDetailFragment :
BaseStateFragment<StreamInfo?>(), BaseStateFragment<StreamInfo>(),
BackPressable, BackPressable,
PlayerServiceExtendedEventListener, PlayerServiceExtendedEventListener,
OnKeyDownListener { OnKeyDownListener {
@@ -160,15 +156,15 @@ class VideoDetailFragment :
private var lastAppBarVerticalOffset = Int.Companion.MAX_VALUE // prevents useless updates private var lastAppBarVerticalOffset = Int.Companion.MAX_VALUE // prevents useless updates
private val preferenceChangeListener = private val preferenceChangeListener =
OnSharedPreferenceChangeListener { sharedPreferences: SharedPreferences?, key: String? -> OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (getString(R.string.show_comments_key) == key) { if (getString(R.string.show_comments_key) == key) {
showComments = sharedPreferences!!.getBoolean(key, true) showComments = sharedPreferences.getBoolean(key, true)
tabSettingsChanged = true tabSettingsChanged = true
} else if (getString(R.string.show_next_video_key) == key) { } else if (getString(R.string.show_next_video_key) == key) {
showRelatedItems = sharedPreferences!!.getBoolean(key, true) showRelatedItems = sharedPreferences.getBoolean(key, true)
tabSettingsChanged = true tabSettingsChanged = true
} else if (getString(R.string.show_description_key) == key) { } else if (getString(R.string.show_description_key) == key) {
showDescription = sharedPreferences!!.getBoolean(key, true) showDescription = sharedPreferences.getBoolean(key, true)
tabSettingsChanged = true tabSettingsChanged = true
} }
} }
@@ -198,11 +194,11 @@ class VideoDetailFragment :
// It will do nothing if the player is not in fullscreen mode // It will do nothing if the player is not in fullscreen mode
hideSystemUiIfNeeded() hideSystemUiIfNeeded()
val mainUi = player?.UIs()?.get(MainPlayerUi::class)
if (player?.videoPlayerSelected() != true && !playAfterConnect) { if (player?.videoPlayerSelected() != true && !playAfterConnect) {
return return
} }
val mainUi = player?.UIs()?.get(MainPlayerUi::class)
if (DeviceUtils.isLandscape(requireContext())) { if (DeviceUtils.isLandscape(requireContext())) {
// If the video is playing but orientation changed // If the video is playing but orientation changed
// let's make the video in fullscreen again // let's make the video in fullscreen again
@@ -252,7 +248,7 @@ class VideoDetailFragment :
setupBroadcastReceiver() setupBroadcastReceiver()
settingsContentObserver = object : ContentObserver(Handler()) { settingsContentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) { override fun onChange(selfChange: Boolean) {
if (activity != null && !PlayerHelper.globalScreenOrientationLocked(activity)) { if (activity != null && !PlayerHelper.globalScreenOrientationLocked(activity)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
@@ -277,9 +273,7 @@ class VideoDetailFragment :
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
if (currentWorker != null) { currentWorker?.dispose()
currentWorker!!.dispose()
}
restoreDefaultBrightness() restoreDefaultBrightness()
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit { PreferenceManager.getDefaultSharedPreferences(requireContext()).edit {
putString( putString(
@@ -304,9 +298,7 @@ class VideoDetailFragment :
if (tabSettingsChanged) { if (tabSettingsChanged) {
tabSettingsChanged = false tabSettingsChanged = false
initTabs() initTabs()
if (currentInfo != null) { currentInfo?.let { updateTabs(it) }
updateTabs(currentInfo!!)
}
} }
// Check if it was loading when the fragment was stopped/paused // Check if it was loading when the fragment was stopped/paused
@@ -339,12 +331,8 @@ class VideoDetailFragment :
activity.unregisterReceiver(broadcastReceiver) activity.unregisterReceiver(broadcastReceiver)
activity.contentResolver.unregisterContentObserver(settingsContentObserver!!) activity.contentResolver.unregisterContentObserver(settingsContentObserver!!)
if (positionSubscriber != null) { positionSubscriber?.dispose()
positionSubscriber!!.dispose() currentWorker?.dispose()
}
if (currentWorker != null) {
currentWorker!!.dispose()
}
disposables.clear() disposables.clear()
positionSubscriber = null positionSubscriber = null
currentWorker = null currentWorker = null
@@ -353,7 +341,7 @@ class VideoDetailFragment :
if (activity.isFinishing) { if (activity.isFinishing) {
playQueue = null playQueue = null
currentInfo = null currentInfo = null
stack = LinkedList<StackItem?>() stack = LinkedList<StackItem>()
} }
} }
@@ -367,8 +355,7 @@ class VideoDetailFragment :
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) { if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
NavigationHelper.openVideoDetailFragment( NavigationHelper.openVideoDetailFragment(
requireContext(), getFM(), requireContext(), getFM(), serviceId, url, title, null, false
serviceId, url, title, null, false
) )
} else { } else {
Log.e(TAG, "ReCaptcha failed") Log.e(TAG, "ReCaptcha failed")
@@ -385,11 +372,11 @@ class VideoDetailFragment :
binding.detailTitleRootLayout.setOnClickListener { toggleTitleAndSecondaryControls() } binding.detailTitleRootLayout.setOnClickListener { toggleTitleAndSecondaryControls() }
binding.detailUploaderRootLayout.setOnClickListener( binding.detailUploaderRootLayout.setOnClickListener(
makeOnClickListener { info -> makeOnClickListener { info ->
if (TextUtils.isEmpty(info.subChannelUrl)) { if (info.subChannelUrl.isEmpty()) {
if (!TextUtils.isEmpty(info.uploaderUrl)) { if (info.uploaderUrl.isNotEmpty()) {
openChannel(info.uploaderUrl, info.uploaderName, info.serviceId) openChannel(info.uploaderUrl, info.uploaderName, info.serviceId)
} else if (DEBUG) { } else if (DEBUG) {
Log.i(TAG, "Can't open sub-channel because we got no channel URL") Log.w(TAG, "Can't open sub-channel because we got no channel URL")
} }
} else { } else {
openChannel(info.subChannelUrl, info.subChannelName, info.serviceId) openChannel(info.subChannelUrl, info.subChannelName, info.serviceId)
@@ -495,7 +482,7 @@ class VideoDetailFragment :
} }
binding.detailUploaderRootLayout.setOnLongClickListener( binding.detailUploaderRootLayout.setOnLongClickListener(
makeOnLongClickListener { info -> makeOnLongClickListener { info ->
if (TextUtils.isEmpty(info.subChannelUrl)) { if (info.subChannelUrl.isEmpty()) {
Log.w(TAG, "Can't open parent channel because we got no parent channel URL") Log.w(TAG, "Can't open parent channel because we got no parent channel URL")
} else { } else {
openChannel(info.uploaderUrl, info.uploaderName, info.serviceId) openChannel(info.uploaderUrl, info.uploaderName, info.serviceId)
@@ -541,7 +528,7 @@ class VideoDetailFragment :
} }
private fun toggleTitleAndSecondaryControls() { private fun toggleTitleAndSecondaryControls() {
if (binding.detailSecondaryControlPanel.visibility == View.GONE) { if (binding.detailSecondaryControlPanel.isGone) {
binding.detailVideoTitleView.setMaxLines(10) binding.detailVideoTitleView.setMaxLines(10)
binding.detailToggleSecondaryControlsView binding.detailToggleSecondaryControlsView
.animateRotation(VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180) .animateRotation(VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180)
@@ -569,18 +556,12 @@ class VideoDetailFragment :
binding.detailThumbnailRootLayout.requestFocus() binding.detailThumbnailRootLayout.requestFocus()
binding.detailControlsPlayWithKodi.visibility = binding.detailControlsPlayWithKodi.isVisible =
if (KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)) KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
View.VISIBLE binding.detailControlsCrashThePlayer.isVisible =
else DEBUG && PreferenceManager.getDefaultSharedPreferences(requireContext())
View.GONE .getBoolean(getString(R.string.show_crash_the_player_key), false)
binding.detailControlsCrashThePlayer.visibility =
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(getString(R.string.show_crash_the_player_key), false)
)
View.VISIBLE
else
View.GONE
accommodateForTvAndDesktopMode() accommodateForTvAndDesktopMode()
} }
@@ -591,8 +572,8 @@ class VideoDetailFragment :
setOnClickListeners() setOnClickListeners()
setOnLongClickListeners() setOnLongClickListeners()
val controlsTouchListener = OnTouchListener { view: View?, motionEvent: MotionEvent? -> val controlsTouchListener = OnTouchListener { view, motionEvent ->
if (motionEvent!!.action == MotionEvent.ACTION_DOWN && if (motionEvent.action == MotionEvent.ACTION_DOWN &&
PlayButtonHelper.shouldShowHoldToAppendTip(activity) PlayButtonHelper.shouldShowHoldToAppendTip(activity)
) { ) {
binding.touchAppendDetail.animate(true, 250, AnimationType.ALPHA, 0) { binding.touchAppendDetail.animate(true, 250, AnimationType.ALPHA, 0) {
@@ -604,16 +585,14 @@ class VideoDetailFragment :
binding.detailControlsBackground.setOnTouchListener(controlsTouchListener) binding.detailControlsBackground.setOnTouchListener(controlsTouchListener)
binding.detailControlsPopup.setOnTouchListener(controlsTouchListener) binding.detailControlsPopup.setOnTouchListener(controlsTouchListener)
binding.appBarLayout.addOnOffsetChangedListener( binding.appBarLayout.addOnOffsetChangedListener { layout, verticalOffset ->
OnOffsetChangedListener { layout: AppBarLayout?, verticalOffset: Int -> // prevent useless updates to tab layout visibility if nothing changed
// prevent useless updates to tab layout visibility if nothing changed if (verticalOffset != lastAppBarVerticalOffset) {
if (verticalOffset != lastAppBarVerticalOffset) { lastAppBarVerticalOffset = verticalOffset
lastAppBarVerticalOffset = verticalOffset // the view was scrolled
// the view was scrolled updateTabLayoutVisibility()
updateTabLayoutVisibility()
}
} }
) }
setupBottomPlayer() setupBottomPlayer()
if (!PlayerHolder.isBound) { if (!PlayerHolder.isBound) {
@@ -656,7 +635,7 @@ class VideoDetailFragment :
// Remove top // Remove top
stack.pop() stack.pop()
// Get stack item from the new top // Get stack item from the new top
setupFromHistoryItem(Objects.requireNonNull<StackItem?>(stack.peek())) setupFromHistoryItem(stack.peek()!!)
return true return true
} }
@@ -688,10 +667,9 @@ class VideoDetailFragment :
return return
} }
currentInfo?.let { info -> when (val info = currentInfo) {
prepareAndHandleInfoIfNeededAfterDelay(info, false, 50) null -> prepareAndLoadInfo()
} ?: { else -> prepareAndHandleInfoIfNeededAfterDelay(info, false, 50)
prepareAndLoadInfo()
} }
} }
@@ -756,9 +734,7 @@ class VideoDetailFragment :
initTabs() initTabs()
currentInfo = null currentInfo = null
if (currentWorker != null) { currentWorker?.dispose()
currentWorker!!.dispose()
}
runWorker(forceLoad, addToBackStack ?: stack.isEmpty()) runWorker(forceLoad, addToBackStack ?: stack.isEmpty())
} }
@@ -783,7 +759,7 @@ class VideoDetailFragment :
if (playQueue == null) { if (playQueue == null) {
playQueue = SinglePlayQueue(result) playQueue = SinglePlayQueue(result)
} }
if (stack.isEmpty() || stack.peek()!!.playQueue != playQueue) { if (stack.peek()?.playQueue != playQueue) { // also if stack empty (!)
stack.push(StackItem(serviceId, url, title, playQueue)) stack.push(StackItem(serviceId, url, title, playQueue))
} }
} }
@@ -866,13 +842,14 @@ class VideoDetailFragment :
private fun updateTabs(info: StreamInfo) { private fun updateTabs(info: StreamInfo) {
if (showRelatedItems) { if (showRelatedItems) {
if (binding.relatedItemsLayout == null) { // phone when (val relatedItemsLayout = binding.relatedItemsLayout) {
pageAdapter.updateItem(RELATED_TAB_TAG, getInstance(info)) null -> pageAdapter.updateItem(RELATED_TAB_TAG, getInstance(info)) // phone
} else { // tablet + TV else -> { // tablet + TV
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.replace(R.id.relatedItemsLayout, getInstance(info)) .replace(R.id.relatedItemsLayout, getInstance(info))
.commitAllowingStateLoss() .commitAllowingStateLoss()
binding.relatedItemsLayout!!.isVisible = !this.isFullscreen relatedItemsLayout.isVisible = !this.isFullscreen
}
} }
} }
@@ -909,8 +886,7 @@ class VideoDetailFragment :
// call `post()` to be sure `viewPager.getHitRect()` // call `post()` to be sure `viewPager.getHitRect()`
// is up to date and not being currently recomputed // is up to date and not being currently recomputed
binding.tabLayout.post { binding.tabLayout.post {
val activity = getActivity() getActivity()?.let { activity ->
if (activity != null) {
val pagerHitRect = Rect() val pagerHitRect = Rect()
binding.viewPager.getHitRect(pagerHitRect) binding.viewPager.getHitRect(pagerHitRect)
@@ -1056,7 +1032,7 @@ class VideoDetailFragment :
} }
private fun openMainPlayer() { private fun openMainPlayer() {
if (noPlayerServiceAvailable()) { if (playerService == null) {
PlayerHolder.startService(autoPlayEnabled, this) PlayerHolder.startService(autoPlayEnabled, this)
return return
} }
@@ -1082,13 +1058,13 @@ class VideoDetailFragment :
*/ */
private fun hideMainPlayerOnLoadingNewStream() { private fun hideMainPlayerOnLoadingNewStream() {
val root = this.root val root = this.root
if (noPlayerServiceAvailable() || root == null || !player!!.videoPlayerSelected()) { if (root == null || playerService == null || player?.videoPlayerSelected() != true) {
return return
} }
removeVideoPlayerView() removeVideoPlayerView()
if (this.isAutoplayEnabled) { if (this.isAutoplayEnabled) {
playerService!!.stopForImmediateReusing() playerService?.stopForImmediateReusing()
root.visibility = View.GONE root.visibility = View.GONE
} else { } else {
PlayerHolder.stopService() PlayerHolder.stopService()
@@ -1127,8 +1103,11 @@ class VideoDetailFragment :
val recordManager = HistoryRecordManager(requireContext()) val recordManager = HistoryRecordManager(requireContext())
disposables.add( disposables.add(
recordManager.onViewed(info).onErrorComplete() recordManager.onViewed(info)
.subscribe({ }, { throwable -> Log.e(TAG, "Register view failure: ", throwable) }) .subscribe(
{ /* successful */ },
{ throwable -> Log.e(TAG, "Register view failure: ", throwable) }
)
) )
} }
@@ -1157,8 +1136,10 @@ class VideoDetailFragment :
if (player == null || view == null) { if (player == null || view == null) {
return@post return@post
} }
// setup the surface view height, so that it fits the video correctly // setup the surface view height, so that it fits the video correctly
setHeightThumbnail() setHeightThumbnail()
player?.UIs()?.get(MainPlayerUi::class)?.let { playerUi -> player?.UIs()?.get(MainPlayerUi::class)?.let { playerUi ->
// sometimes binding would be null here, even though getView() != null above u.u // sometimes binding would be null here, even though getView() != null above u.u
nullableBinding?.let { b -> nullableBinding?.let { b ->
@@ -1186,13 +1167,13 @@ class VideoDetailFragment :
} }
private val preDrawListener: OnPreDrawListener = OnPreDrawListener { private val preDrawListener: OnPreDrawListener = OnPreDrawListener {
if (view != null) { view?.let { view ->
val decorView = if (DeviceUtils.isInMultiWindow(activity)) val decorView = if (DeviceUtils.isInMultiWindow(activity))
requireView() view
else else
activity.window.decorView activity.window.decorView
setHeightThumbnail(decorView.height, resources.displayMetrics) setHeightThumbnail(decorView.height, resources.displayMetrics)
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener) view.getViewTreeObserver().removeOnPreDrawListener(preDrawListener)
} }
return@OnPreDrawListener false return@OnPreDrawListener false
} }
@@ -1366,20 +1347,20 @@ class VideoDetailFragment :
binding.relatedItemsLayout?.isVisible = showRelatedItems && !this.isFullscreen binding.relatedItemsLayout?.isVisible = showRelatedItems && !this.isFullscreen
dispose(binding.detailThumbnailImageView) CoilUtils.dispose(binding.detailThumbnailImageView)
dispose(binding.detailSubChannelThumbnailView) CoilUtils.dispose(binding.detailSubChannelThumbnailView)
dispose(binding.overlayThumbnail) CoilUtils.dispose(binding.overlayThumbnail)
dispose(binding.detailUploaderThumbnailView) CoilUtils.dispose(binding.detailUploaderThumbnailView)
binding.detailThumbnailImageView.setImageBitmap(null) binding.detailThumbnailImageView.setImageBitmap(null)
binding.detailSubChannelThumbnailView.setImageBitmap(null) binding.detailSubChannelThumbnailView.setImageBitmap(null)
} }
override fun handleResult(info: StreamInfo?) { override fun handleResult(info: StreamInfo) {
super.handleResult(info) super.handleResult(info)
currentInfo = info currentInfo = info
setInitialData(info!!.serviceId, info.originalUrl, info.name, playQueue) setInitialData(info.serviceId, info.originalUrl, info.name, playQueue)
updateTabs(info) updateTabs(info)
@@ -1388,10 +1369,10 @@ class VideoDetailFragment :
binding.detailSubChannelThumbnailView.visibility = View.GONE binding.detailSubChannelThumbnailView.visibility = View.GONE
if (!TextUtils.isEmpty(info.subChannelName)) { if (info.subChannelName.isEmpty()) {
displayBothUploaderAndSubChannel(info)
} else {
displayUploaderAsSubChannel(info) displayUploaderAsSubChannel(info)
} else {
displayBothUploaderAndSubChannel(info)
} }
if (info.viewCount >= 0) { if (info.viewCount >= 0) {
@@ -1459,10 +1440,7 @@ class VideoDetailFragment :
binding.detailSecondaryControlPanel.visibility = View.GONE binding.detailSecondaryControlPanel.visibility = View.GONE
checkUpdateProgressInfo(info) checkUpdateProgressInfo(info)
loadDetailsThumbnail( CoilHelper.loadDetailsThumbnail(binding.detailThumbnailImageView, info.thumbnails)
binding.detailThumbnailImageView,
info.thumbnails
)
ExtractorHelper.showMetaInfoInTextView( ExtractorHelper.showMetaInfoInTextView(
info.metaInfo, binding.detailMetaInfoTextView, info.metaInfo, binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables binding.detailMetaInfoSeparator, disposables
@@ -1475,12 +1453,8 @@ class VideoDetailFragment :
if (!info.errors.isEmpty()) { if (!info.errors.isEmpty()) {
// Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is // Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is
// thrown. This is not an error and thus should not be shown to the user. // thrown. This is not an error and thus should not be shown to the user.
for (throwable in info.errors) { info.errors.removeIf {
if (throwable is ContentNotSupportedException && it is ContentNotSupportedException && "Fan pages are not supported" == it.message
"Fan pages are not supported" == throwable.message
) {
info.errors.remove(throwable)
}
} }
if (!info.errors.isEmpty()) { if (!info.errors.isEmpty()) {
@@ -1490,8 +1464,9 @@ class VideoDetailFragment :
} }
} }
val hasAudioStreams = info.videoStreams.isNotEmpty() || info.audioStreams.isNotEmpty()
binding.detailControlsDownload.isVisible = !StreamTypeUtil.isLiveStream(info.streamType) binding.detailControlsDownload.isVisible = !StreamTypeUtil.isLiveStream(info.streamType)
val hasAudioStreams = info.videoStreams.isNotEmpty() || info.audioStreams.isNotEmpty()
binding.detailControlsBackground.isVisible = hasAudioStreams binding.detailControlsBackground.isVisible = hasAudioStreams
val hasVideoStreams = info.videoStreams.isNotEmpty() || info.videoOnlyStreams.isNotEmpty() val hasVideoStreams = info.videoStreams.isNotEmpty() || info.videoOnlyStreams.isNotEmpty()
@@ -1514,7 +1489,7 @@ class VideoDetailFragment :
binding.detailUploaderTextView.visibility = View.GONE binding.detailUploaderTextView.visibility = View.GONE
} }
loadAvatar(binding.detailSubChannelThumbnailView, info.uploaderAvatars) CoilHelper.loadAvatar(binding.detailSubChannelThumbnailView, info.uploaderAvatars)
binding.detailSubChannelThumbnailView.visibility = View.VISIBLE binding.detailSubChannelThumbnailView.visibility = View.VISIBLE
binding.detailUploaderThumbnailView.visibility = View.GONE binding.detailUploaderThumbnailView.visibility = View.GONE
} }
@@ -1525,8 +1500,8 @@ class VideoDetailFragment :
binding.detailSubChannelTextView.setSelected(true) binding.detailSubChannelTextView.setSelected(true)
val subText = StringBuilder() val subText = StringBuilder()
if (!TextUtils.isEmpty(info.uploaderName)) { if (info.uploaderName.isNotEmpty()) {
subText.append(String.format(getString(R.string.video_detail_by), info.uploaderName)) subText.append(getString(R.string.video_detail_by, info.uploaderName))
} }
if (info.uploaderSubscriberCount > -1) { if (info.uploaderSubscriberCount > -1) {
if (subText.isNotEmpty()) { if (subText.isNotEmpty()) {
@@ -1545,26 +1520,22 @@ class VideoDetailFragment :
binding.detailUploaderTextView.setSelected(true) binding.detailUploaderTextView.setSelected(true)
} }
loadAvatar(binding.detailSubChannelThumbnailView, info.subChannelAvatars) CoilHelper.loadAvatar(binding.detailSubChannelThumbnailView, info.subChannelAvatars)
binding.detailSubChannelThumbnailView.visibility = View.VISIBLE binding.detailSubChannelThumbnailView.visibility = View.VISIBLE
loadAvatar(binding.detailUploaderThumbnailView, info.uploaderAvatars) CoilHelper.loadAvatar(binding.detailUploaderThumbnailView, info.uploaderAvatars)
binding.detailUploaderThumbnailView.visibility = View.VISIBLE binding.detailUploaderThumbnailView.visibility = View.VISIBLE
} }
fun openDownloadDialog() { fun openDownloadDialog() {
if (currentInfo == null) { val info = currentInfo ?: return
return
}
try { try {
val downloadDialog = DownloadDialog(activity, currentInfo!!) val downloadDialog = DownloadDialog(activity, info)
downloadDialog.show(activity.supportFragmentManager, "downloadDialog") downloadDialog.show(activity.supportFragmentManager, "downloadDialog")
} catch (e: Exception) { } catch (e: Exception) {
showSnackbar( showSnackbar(
activity, activity,
ErrorInfo( ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog", info)
e, UserAction.DOWNLOAD_OPEN_DIALOG, "Showing download dialog", currentInfo
)
) )
} }
} }
@@ -1586,7 +1557,7 @@ class VideoDetailFragment :
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ state -> updatePlaybackProgress(state.progressMillis, info.duration * 1000) }, { state -> updatePlaybackProgress(state.progressMillis, info.duration * 1000) },
{ throwable -> /* ignore errors */ }, { throwable -> /* impossible due to the onErrorComplete() */ },
{ /* onComplete */ { /* onComplete */
binding.positionView.visibility = View.GONE binding.positionView.visibility = View.GONE
binding.detailPositionView.visibility = View.GONE binding.detailPositionView.visibility = View.GONE
@@ -1600,11 +1571,10 @@ class VideoDetailFragment :
} }
val progressSeconds = TimeUnit.MILLISECONDS.toSeconds(progress).toInt() val progressSeconds = TimeUnit.MILLISECONDS.toSeconds(progress).toInt()
val durationSeconds = TimeUnit.MILLISECONDS.toSeconds(duration).toInt() val durationSeconds = TimeUnit.MILLISECONDS.toSeconds(duration).toInt()
binding.positionView.setMax(durationSeconds)
// If the old and the new progress values have a big difference then use animation. // If the old and the new progress values have a big difference then use animation.
// Otherwise don't because it affects CPU // Otherwise don't because it affects CPU
val progressDifference = abs(binding.positionView.progress - progressSeconds) if (abs(binding.positionView.progress - progressSeconds) > 2) {
binding.positionView.setMax(durationSeconds)
if (progressDifference > 2) {
binding.positionView.setProgressAnimated(progressSeconds) binding.positionView.setProgressAnimated(progressSeconds)
} else { } else {
binding.positionView.progress = progressSeconds binding.positionView.progress = progressSeconds
@@ -1637,15 +1607,15 @@ class VideoDetailFragment :
} }
// Register broadcast receiver to listen to playQueue changes // Register broadcast receiver to listen to playQueue changes
// and hide the overlayPlayQueueButton when the playQueue is empty / destroyed. // and hide the overlayPlayQueueButton when the playQueue is empty / destroyed.7
playQueue?.broadcastReceiver?.subscribe { updateOverlayPlayQueueButtonVisibility() } playQueue?.broadcastReceiver?.subscribe { updateOverlayPlayQueueButtonVisibility() }
?.let { disposables.add(it) }
// This should be the only place where we push data to stack. // This should be the only place where we push data to stack.
// It will allow to have live instance of PlayQueue with actual information about // It will allow to have live instance of PlayQueue with actual information about
// deleted/added items inside Channel/Playlist queue and makes possible to have // deleted/added items inside Channel/Playlist queue and makes possible to have
// a history of played items // a history of played items
val stackPeek: StackItem? = stack.peek() if (stack.peek()?.playQueue?.equals(queue) == false) {
if (stackPeek != null && stackPeek.playQueue != queue) {
queue.item?.let { queueItem -> queue.item?.let { queueItem ->
stack.push(StackItem(queueItem.serviceId, queueItem.url, queueItem.title, queue)) stack.push(StackItem(queueItem.serviceId, queueItem.url, queueItem.title, queue))
return@onQueueUpdate return@onQueueUpdate
@@ -1703,7 +1673,7 @@ class VideoDetailFragment :
} }
updateOverlayData(info.name, info.uploaderName, info.thumbnails) updateOverlayData(info.name, info.uploaderName, info.thumbnails)
if (currentInfo?.url == info.url) { if (info.url == currentInfo?.url) {
return return
} }
@@ -1780,17 +1750,15 @@ class VideoDetailFragment :
* Will scroll down to description view after long click on moreOptionsButton * Will scroll down to description view after long click on moreOptionsButton
* */ * */
override fun onMoreOptionsLongClicked() { override fun onMoreOptionsLongClicked() {
val params = val params = binding.appBarLayout.layoutParams as CoordinatorLayout.LayoutParams
binding.appBarLayout.layoutParams as CoordinatorLayout.LayoutParams
val behavior = params.behavior as AppBarLayout.Behavior val behavior = params.behavior as AppBarLayout.Behavior
val valueAnimator = ValueAnimator.ofInt(0, -binding.playerPlaceholder.height) val valueAnimator = ValueAnimator.ofInt(0, -binding.playerPlaceholder.height)
valueAnimator.interpolator = DecelerateInterpolator()
valueAnimator.addUpdateListener { animation -> valueAnimator.addUpdateListener { animation ->
behavior.setTopAndBottomOffset(animation.getAnimatedValue() as Int) behavior.setTopAndBottomOffset(animation.getAnimatedValue() as Int)
binding.appBarLayout.requestLayout() binding.appBarLayout.requestLayout()
} }
valueAnimator.interpolator = DecelerateInterpolator() valueAnimator.interpolator = DecelerateInterpolator()
valueAnimator.setDuration(500) valueAnimator.duration = 500
valueAnimator.start() valueAnimator.start()
} }
@@ -1867,9 +1835,11 @@ class VideoDetailFragment :
private val isFullscreen: Boolean private val isFullscreen: Boolean
get() = player?.UIs()?.get(VideoPlayerUi::class)?.isFullscreen == true get() = player?.UIs()?.get(VideoPlayerUi::class)?.isFullscreen == true
/**
* @return true if the player is null, or if the player is nonnull but is stopped.
*/
@Suppress("NullableBooleanElvis") // rewriting as "!= false" creates more confusion @Suppress("NullableBooleanElvis") // rewriting as "!= false" creates more confusion
private val playerIsStopped private val playerIsStopped
// returns true if the player is null, or if the player is nonnull but is stopped
get() = player?.isStopped ?: true get() = player?.isStopped ?: true
private fun restoreDefaultBrightness() { private fun restoreDefaultBrightness() {
@@ -2017,9 +1987,8 @@ class VideoDetailFragment :
.map { it.getResolution() as CharSequence } .map { it.getResolution() as CharSequence }
.toTypedArray() .toTypedArray()
builder.setSingleChoiceItems( builder
resolutions, selectedVideoStreamIndexForExternalPlayers, null .setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers, null)
)
builder.setNegativeButton(R.string.cancel, null) builder.setNegativeButton(R.string.cancel, null)
builder.setPositiveButton(R.string.ok) { dialog, which -> builder.setPositiveButton(R.string.ok) { dialog, which ->
val index = (dialog as AlertDialog).listView.getCheckedItemPosition() val index = (dialog as AlertDialog).listView.getCheckedItemPosition()
@@ -2050,16 +2019,14 @@ class VideoDetailFragment :
startOnExternalPlayer(activity, info, audioTracks[0]) startOnExternalPlayer(activity, info, audioTracks[0])
} else { } else {
val selectedAudioStream = ListHelper.getDefaultAudioFormat(activity, audioTracks) val selectedAudioStream = ListHelper.getDefaultAudioFormat(activity, audioTracks)
val trackNames = audioTracks val trackNames = audioTracks.map { Localization.audioTrackName(activity, it) }
.map { Localization.audioTrackName(activity, it) }
.toTypedArray()
AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setTitle(R.string.select_audio_track_external_players) .setTitle(R.string.select_audio_track_external_players)
.setNeutralButton(R.string.open_in_browser) { dialog, which -> .setNeutralButton(R.string.open_in_browser) { dialog, which ->
ShareUtils.openUrlInBrowser(requireActivity(), url) ShareUtils.openUrlInBrowser(requireActivity(), url)
} }
.setSingleChoiceItems(trackNames, selectedAudioStream, null) .setSingleChoiceItems(trackNames.toTypedArray(), selectedAudioStream, null)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { dialog, which -> .setPositiveButton(R.string.ok) { dialog, which ->
val index = (dialog as AlertDialog).listView.getCheckedItemPosition() val index = (dialog as AlertDialog).listView.getCheckedItemPosition()
@@ -2079,7 +2046,7 @@ class VideoDetailFragment :
PlayerHolder.stopService() PlayerHolder.stopService()
setInitialData(0, null, "", null) setInitialData(0, null, "", null)
currentInfo = null currentInfo = null
updateOverlayData(null, null, mutableListOf<Image>()) updateOverlayData(null, null, listOf())
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@@ -2145,8 +2112,8 @@ class VideoDetailFragment :
val behavior = params.behavior as AppBarLayout.Behavior? val behavior = params.behavior as AppBarLayout.Behavior?
val bottomSheetLayout = activity.findViewById<FrameLayout>(R.id.fragment_player_holder) val bottomSheetLayout = activity.findViewById<FrameLayout>(R.id.fragment_player_holder)
bottomSheetBehavior = BottomSheetBehavior.from<FrameLayout?>(bottomSheetLayout) bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout)
bottomSheetBehavior.setState(lastStableBottomSheetState) bottomSheetBehavior.state = lastStableBottomSheetState
updateBottomSheetState(lastStableBottomSheetState) updateBottomSheetState(lastStableBottomSheetState)
val peekHeight = resources.getDimensionPixelSize(R.dimen.mini_player_height) val peekHeight = resources.getDimensionPixelSize(R.dimen.mini_player_height)
@@ -2154,9 +2121,9 @@ class VideoDetailFragment :
manageSpaceAtTheBottom(false) manageSpaceAtTheBottom(false)
bottomSheetBehavior.peekHeight = peekHeight bottomSheetBehavior.peekHeight = peekHeight
if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) { if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) {
binding.overlayLayout.setAlpha(MAX_OVERLAY_ALPHA) binding.overlayLayout.alpha = MAX_OVERLAY_ALPHA
} else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) { } else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) {
binding.overlayLayout.setAlpha(0f) binding.overlayLayout.alpha = 0f
setOverlayElementsClickable(false) setOverlayElementsClickable(false)
} }
} }
@@ -2189,7 +2156,7 @@ class VideoDetailFragment :
!this@VideoDetailFragment.isFullscreen && !this@VideoDetailFragment.isFullscreen &&
!DeviceUtils.isTablet(activity) !DeviceUtils.isTablet(activity)
) { ) {
player!!.UIs().get(MainPlayerUi::class)?.toggleFullscreen() player?.UIs()?.get(MainPlayerUi::class)?.toggleFullscreen()
} }
setOverlayLook(binding.appBarLayout, behavior, 1f) setOverlayLook(binding.appBarLayout, behavior, 1f)
} }
@@ -2238,18 +2205,18 @@ class VideoDetailFragment :
private fun updateOverlayPlayQueueButtonVisibility() { private fun updateOverlayPlayQueueButtonVisibility() {
// hide the button if the queue is empty; no player => no play queue :) // hide the button if the queue is empty; no player => no play queue :)
nullableBinding?.overlayPlayQueueButton?.isVisible = player?.playQueue?.isEmpty != true nullableBinding?.overlayPlayQueueButton?.isVisible = player?.playQueue?.isEmpty == false
} }
private fun updateOverlayData( private fun updateOverlayData(
overlayTitle: String?, overlayTitle: String?,
uploader: String?, uploader: String?,
thumbnails: MutableList<Image> thumbnails: List<Image>
) { ) {
binding.overlayTitleTextView.text = overlayTitle ?: "" binding.overlayTitleTextView.text = overlayTitle ?: ""
binding.overlayChannelTextView.text = uploader ?: "" binding.overlayChannelTextView.text = uploader ?: ""
binding.overlayThumbnail.setImageDrawable(null) binding.overlayThumbnail.setImageDrawable(null)
loadDetailsThumbnail(binding.overlayThumbnail, thumbnails) CoilHelper.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails)
} }
private fun setOverlayPlayPauseImage(playerIsPlaying: Boolean) { private fun setOverlayPlayPauseImage(playerIsPlaying: Boolean) {
@@ -2267,12 +2234,7 @@ class VideoDetailFragment :
if (behavior == null || slideOffset < 0) { if (behavior == null || slideOffset < 0) {
return return
} }
binding.overlayLayout.setAlpha( binding.overlayLayout.alpha = min(MAX_OVERLAY_ALPHA, 1 - slideOffset)
min(
MAX_OVERLAY_ALPHA.toDouble(),
(1 - slideOffset).toDouble()
).toFloat()
)
// These numbers are not special. They just do a cool transition // These numbers are not special. They just do a cool transition
behavior.setTopAndBottomOffset( behavior.setTopAndBottomOffset(
(-binding.detailThumbnailImageView.height * 2 * (1 - slideOffset) / 3).toInt() (-binding.detailThumbnailImageView.height * 2 * (1 - slideOffset) / 3).toInt()
@@ -2291,10 +2253,6 @@ class VideoDetailFragment :
binding.overlayCloseButton.isClickable = enable binding.overlayCloseButton.isClickable = enable
} }
fun noPlayerServiceAvailable(): Boolean {
return playerService == null
}
val root: View? val root: View?
get() = player?.UIs()?.get(VideoPlayerUi::class)?.binding?.root get() = player?.UIs()?.get(VideoPlayerUi::class)?.binding?.root
@@ -2356,6 +2314,6 @@ class VideoDetailFragment :
* Stack that contains the "navigation history".<br></br> * Stack that contains the "navigation history".<br></br>
* The peek is the current video. * The peek is the current video.
*/ */
private var stack = LinkedList<StackItem?>() private var stack = LinkedList<StackItem>()
} }
} }

View File

@@ -9,6 +9,7 @@ import android.support.v4.media.MediaDescriptionCompat
import android.util.Log import android.util.Log
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import androidx.media.MediaBrowserServiceCompat.Result import androidx.media.MediaBrowserServiceCompat.Result
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
@@ -180,17 +181,16 @@ class MediaBrowserImpl(
private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem { private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem {
val builder = MediaDescriptionCompat.Builder() val builder = MediaDescriptionCompat.Builder()
builder
.setMediaId(createMediaIdForInfoItem(playlist is PlaylistRemoteEntity, playlist.uid)) .setMediaId(createMediaIdForInfoItem(playlist is PlaylistRemoteEntity, playlist.uid))
.setTitle(playlist.orderingName) .setTitle(playlist.orderingName)
.setIconUri(imageUriOrNullIfDisabled(playlist.thumbnailUrl)) .setIconUri(imageUriOrNullIfDisabled(playlist.thumbnailUrl))
.setExtras(
bundleOf(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE
to context.resources.getString(R.string.tab_bookmarks)
)
)
val extras = Bundle()
extras.putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
context.resources.getString(R.string.tab_bookmarks),
)
builder.setExtras(extras)
return MediaBrowserCompat.MediaItem( return MediaBrowserCompat.MediaItem(
builder.build(), builder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE,
@@ -199,7 +199,7 @@ class MediaBrowserImpl(
private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem? { private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem? {
val builder = MediaDescriptionCompat.Builder() val builder = MediaDescriptionCompat.Builder()
builder.setMediaId(createMediaIdForInfoItem(item)) .setMediaId(createMediaIdForInfoItem(item))
.setTitle(item.name) .setTitle(item.name)
.setIconUri(ImageStrategy.choosePreferredImage(item.thumbnails)?.toUri()) .setIconUri(ImageStrategy.choosePreferredImage(item.thumbnails)?.toUri())
@@ -250,7 +250,7 @@ class MediaBrowserImpl(
index: Int, index: Int,
): MediaBrowserCompat.MediaItem { ): MediaBrowserCompat.MediaItem {
val builder = MediaDescriptionCompat.Builder() val builder = MediaDescriptionCompat.Builder()
builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index)) .setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
.setTitle(item.streamEntity.title) .setTitle(item.streamEntity.title)
.setSubtitle(item.streamEntity.uploader) .setSubtitle(item.streamEntity.uploader)
.setIconUri(imageUriOrNullIfDisabled(item.streamEntity.thumbnailUrl)) .setIconUri(imageUriOrNullIfDisabled(item.streamEntity.thumbnailUrl))