diff --git a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt index d17aecbfb..3fd1c1a6a 100644 --- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt @@ -838,6 +838,9 @@ object PlayerHelper { } } + /** + * Handle basic [PlayerEvent]'s that can be handled by the player itself without context + */ fun handlePlayerAction(player: Player, playerEvent: PlayerEvent): Boolean { return when (playerEvent) { PlayerEvent.PlayPause -> { diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 306348b45..ee671528f 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -14,7 +14,6 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.PowerManager -import android.text.format.DateUtils import android.view.KeyEvent import android.view.LayoutInflater import android.view.View @@ -32,7 +31,6 @@ import androidx.core.os.postDelayed import androidx.core.view.SoftwareKeyboardControllerCompat import androidx.core.view.WindowCompat import androidx.core.view.isGone -import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -102,7 +100,6 @@ import com.github.libretube.ui.models.CommentsViewModel import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.sheets.BaseBottomSheet import com.github.libretube.ui.sheets.CommentsSheet -import com.github.libretube.ui.sheets.PlayingQueueSheet import com.github.libretube.ui.sheets.StatsSheet import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.OnlineTimeFrameReceiver @@ -160,9 +157,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { Executors.newCachedThreadPool() ) - // SponsorBlock - private var sponsorBlockEnabled = PlayerHelper.sponsorBlockEnabled - private val handler = Handler(Looper.getMainLooper()) private var seekBarPreviewListener: SeekbarPreviewListener? = null @@ -271,7 +265,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } override fun onEvents(player: Player, events: Player.Events) { - updateDisplayedDuration() super.onEvents(player, events) if (events.containsAny( @@ -522,7 +515,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { playerBinding.closeImageButton.setOnClickListener { onManualPlayerClose() } - playerBinding.autoPlay.isVisible = true binding.playImageView.setOnClickListener { exoPlayer.togglePlayPauseState() @@ -540,28 +532,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { CommentsSheet().show(childFragmentManager) } - playerBinding.queueToggle.isVisible = true - playerBinding.queueToggle.setOnClickListener { - PlayingQueueSheet().show(childFragmentManager, null) - } - // FullScreen button trigger // hide fullscreen button if autorotation enabled playerBinding.fullscreen.setOnClickListener { toggleFullscreen() } - val updateSbImageResource = { - playerBinding.sbToggle.setImageResource( - if (sponsorBlockEnabled) R.drawable.ic_sb_enabled else R.drawable.ic_sb_disabled - ) - } - updateSbImageResource() - playerBinding.sbToggle.setOnClickListener { - sponsorBlockEnabled = !sponsorBlockEnabled - updateSbImageResource() - } - // share button binding.relPlayerShare.setOnClickListener { if (!this::streams.isInitialized) return@setOnClickListener @@ -695,16 +671,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { updateFullscreenOrientation() commentsViewModel.setCommentSheetExpand(null) - playerBinding.fullscreen.setImageResource(R.drawable.ic_fullscreen_exit) - playerBinding.exoTitle.isVisible = true updateResolutionOnFullscreenChange(true) openOrCloseFullscreenDialog(true) binding.player.updateMarginsByFullscreenMode() - - updateFullscreenButtonVisibility() } @SuppressLint("SourceLockedOrientationActivity") @@ -718,8 +690,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } viewModel.isFullscreen.value = false - playerBinding.fullscreen.setImageResource(R.drawable.ic_fullscreen) - playerBinding.exoTitle.isInvisible = true if (!PlayerHelper.autoFullscreenEnabled) { mainActivity.requestedOrientation = mainActivity.screenOrientationPref @@ -729,12 +699,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { updateResolutionOnFullscreenChange(false) binding.player.updateMarginsByFullscreenMode() - - updateFullscreenButtonVisibility() - } - - private fun updateFullscreenButtonVisibility() { - playerBinding.fullscreen.isInvisible = PlayerHelper.autoFullscreenEnabled } /** @@ -881,7 +845,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { if (!exoPlayer.isPlaying || !PlayerHelper.sponsorBlockEnabled) return handler.postDelayed(this::checkForSegments, 100) - if (!sponsorBlockEnabled || viewModel.segments.isEmpty()) return + if (!viewModel.sponsorBlockEnabled || viewModel.segments.isEmpty()) return exoPlayer.checkForSegments( requireContext(), @@ -949,7 +913,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { ) { setFullscreen() } - updateFullscreenButtonVisibility() binding.player.apply { useController = false @@ -1000,7 +963,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { withContext(Dispatchers.Main) { playerBinding.exoProgress.setSegments(viewModel.segments) playerBinding.sbToggle.isVisible = true - updateDisplayedDuration() } viewModel.segments.firstOrNull { it.category == PlayerHelper.SPONSOR_HIGHLIGHT_CATEGORY } ?.let { @@ -1082,8 +1044,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { this.streams.uploader ) - syncQueueButtons() - // seekbar preview setup playerBinding.seekbarPreview.isGone = true seekBarPreviewListener?.let { playerBinding.exoProgress.removeListener(it) } @@ -1137,39 +1097,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } } - /** - * Update the displayed duration of the video - */ - private fun updateDisplayedDuration() { - if (!this::streams.isInitialized || streams.livestream || _binding == null) return - - val duration = exoPlayer.duration / 1000 - if (duration < 0) return - - val durationWithoutSegments = duration - viewModel.segments.sumOf { - val (start, end) = it.segmentStartAndEnd - end.toDouble() - start.toDouble() - }.toLong() - val durationString = DateUtils.formatElapsedTime(duration) - - playerBinding.duration.text = if (durationWithoutSegments < duration) { - "$durationString (${DateUtils.formatElapsedTime(durationWithoutSegments)})" - } else { - durationString - } - } - - private fun syncQueueButtons() { - if (!PlayerHelper.skipButtonsEnabled) return - - // toggle the visibility of next and prev buttons based on queue and whether the player view is locked - val isPlayerLocked = binding.player.isPlayerLocked - playerBinding.skipPrev.isInvisible = !PlayingQueue.hasPrev() || isPlayerLocked - playerBinding.skipNext.isInvisible = !PlayingQueue.hasNext() || isPlayerLocked - - handler.postDelayed(this::syncQueueButtons, 100) - } - private fun updatePlayPauseButton() { binding.playImageView.setImageResource(PlayerHelper.getPlayPauseActionIcon(exoPlayer)) } diff --git a/app/src/main/java/com/github/libretube/ui/models/PlayerViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/PlayerViewModel.kt index dcd0befc5..9901e9ea3 100644 --- a/app/src/main/java/com/github/libretube/ui/models/PlayerViewModel.kt +++ b/app/src/main/java/com/github/libretube/ui/models/PlayerViewModel.kt @@ -15,7 +15,9 @@ import com.github.libretube.api.obj.Message import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Subtitle +import com.github.libretube.constants.PreferenceKeys import com.github.libretube.helpers.PlayerHelper +import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.deArrow import java.io.IOException @@ -51,6 +53,7 @@ class PlayerViewModel : ViewModel() { val chaptersLiveData = MutableLiveData>() val chapters get() = chaptersLiveData.value.orEmpty() + var sponsorBlockEnabled = PlayerHelper.sponsorBlockEnabled /** * @return pair of the stream info and the error message if the request was not successful diff --git a/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt index 90b4df5ff..5b02b5aff 100644 --- a/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt +++ b/app/src/main/java/com/github/libretube/ui/views/CustomExoPlayerView.kt @@ -170,20 +170,7 @@ abstract class CustomExoPlayerView( player?.addListener(object : Player.Listener { override fun onEvents(player: Player, events: Player.Events) { super.onEvents(player, events) - if (events.containsAny( - Player.EVENT_PLAYBACK_STATE_CHANGED, - Player.EVENT_IS_PLAYING_CHANGED, - Player.EVENT_PLAY_WHEN_READY_CHANGED - ) - ) { - binding.playPauseBTN.setImageResource( - PlayerHelper.getPlayPauseActionIcon(player) - ) - - // keep screen on if the video is playing - keepScreenOn = player.isPlaying == true - onPlayerEvent(player, events) - } + this@CustomExoPlayerView.onPlaybackEvents(player, events) } }) @@ -820,6 +807,23 @@ abstract class CustomExoPlayerView( return true } + open fun onPlaybackEvents(player: Player, events: Player.Events) { + if (events.containsAny( + Player.EVENT_PLAYBACK_STATE_CHANGED, + Player.EVENT_IS_PLAYING_CHANGED, + Player.EVENT_PLAY_WHEN_READY_CHANGED + ) + ) { + binding.playPauseBTN.setImageResource( + PlayerHelper.getPlayPauseActionIcon(player) + ) + + // keep screen on if the video is playing + keepScreenOn = player.isPlaying == true + onPlayerEvent(player, events) + } + } + open fun minimizeOrExitPlayer() = Unit abstract fun getChapters(): List diff --git a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt index 083b85ac2..e4df40a70 100644 --- a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt +++ b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt @@ -2,12 +2,15 @@ package com.github.libretube.ui.views import android.content.Context import android.os.Bundle +import android.text.format.DateUtils import android.util.AttributeSet import android.view.Window import androidx.core.os.bundleOf +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner import androidx.media3.common.C +import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.trackselection.TrackSelector import com.github.libretube.R @@ -24,6 +27,7 @@ import com.github.libretube.ui.dialogs.SubmitDeArrowDialog import com.github.libretube.ui.dialogs.SubmitSegmentDialog import com.github.libretube.ui.interfaces.OnlinePlayerOptions import com.github.libretube.ui.models.PlayerViewModel +import com.github.libretube.ui.sheets.PlayingQueueSheet import com.github.libretube.util.PlayingQueue @UnstableApi @@ -154,14 +158,39 @@ class OnlinePlayerView( playerViewModel.isFullscreen.observe(viewLifecycleOwner) { isFullscreen -> WindowHelper.toggleFullscreen(activity.window, isFullscreen) updateTopBarMargin() + + binding.fullscreen.isInvisible = PlayerHelper.autoFullscreenEnabled + val fullscreenDrawable = if (isFullscreen) R.drawable.ic_fullscreen_exit else R.drawable.ic_fullscreen + binding.fullscreen.setImageResource(fullscreenDrawable) + + binding.exoTitle.isInvisible = !isFullscreen } + binding.autoPlay.isVisible = true binding.autoPlay.isChecked = PlayerHelper.autoPlayEnabled binding.autoPlay.setOnCheckedChangeListener { _, isChecked -> PlayerHelper.autoPlayEnabled = isChecked } + binding.queueToggle.isVisible = true + binding.queueToggle.setOnClickListener { + PlayingQueueSheet().show(activity.supportFragmentManager, null) + } + + val updateSbImageResource = { + binding.sbToggle.setImageResource( + if (playerViewModel.sponsorBlockEnabled) R.drawable.ic_sb_enabled else R.drawable.ic_sb_disabled + ) + } + updateSbImageResource() + binding.sbToggle.setOnClickListener { + playerViewModel.sponsorBlockEnabled = !playerViewModel.sponsorBlockEnabled + updateSbImageResource() + } + + syncQueueButtons() + binding.sbSubmit.isVisible = PreferenceHelper.getBoolean(PreferenceKeys.CONTRIBUTE_TO_SB, false) binding.sbSubmit.setOnClickListener { val submitSegmentDialog = SubmitSegmentDialog() @@ -189,6 +218,38 @@ class OnlinePlayerView( ) } + private fun syncQueueButtons() { + if (!PlayerHelper.skipButtonsEnabled) return + + // toggle the visibility of next and prev buttons based on queue and whether the player view is locked + binding.skipPrev.isInvisible = !PlayingQueue.hasPrev() || isPlayerLocked + binding.skipNext.isInvisible = !PlayingQueue.hasNext() || isPlayerLocked + + handler.postDelayed(this::syncQueueButtons, 100) + } + + /** + * Update the displayed duration of the video + */ + private fun updateDisplayedDuration() { + if (isLive) return + + val duration = player?.duration?.div(1000) ?: return + if (duration < 0) return + + val durationWithoutSegments = duration - playerViewModel?.segments.orEmpty().sumOf { + val (start, end) = it.segmentStartAndEnd + end.toDouble() - start.toDouble() + }.toLong() + val durationString = DateUtils.formatElapsedTime(duration) + + binding.duration.text = if (durationWithoutSegments < duration) { + "$durationString (${DateUtils.formatElapsedTime(durationWithoutSegments)})" + } else { + durationString + } + } + override fun getWindow(): Window = currentWindow ?: activity.window override fun hideController() { @@ -219,4 +280,9 @@ class OnlinePlayerView( override fun getChapters(): List { return playerViewModel?.chapters.orEmpty() } + + override fun onPlaybackEvents(player: Player, events: Player.Events) { + super.onPlaybackEvents(player, events) + updateDisplayedDuration() + } }