diff --git a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt index 5a1b76ad8..2e4a60c0c 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt @@ -5,13 +5,9 @@ import android.media.session.PlaybackState import android.net.Uri import android.os.Bundle import android.text.format.DateUtils -import android.view.View -import android.view.ViewGroup.MarginLayoutParams import androidx.activity.viewModels import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.core.view.marginTop -import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import androidx.media3.common.C import androidx.media3.common.MediaItem @@ -30,9 +26,7 @@ import com.github.libretube.constants.IntentData import com.github.libretube.databinding.ActivityOfflinePlayerBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding import com.github.libretube.db.DatabaseHolder.Database -import com.github.libretube.db.obj.DownloadItem import com.github.libretube.enums.FileType -import com.github.libretube.extensions.dpToPx import com.github.libretube.extensions.toAndroidUri import com.github.libretube.extensions.updateParameters import com.github.libretube.helpers.PlayerHelper @@ -105,24 +99,14 @@ class OfflinePlayerActivity : BaseActivity() { playerView.player = player playerBinding = binding.player.binding - // increase the margin to the status bar - playerBinding.topBar.setPadding( - playerBinding.topBar.paddingLeft, - playerBinding.topBar.paddingTop * 2, - playerBinding.topBar.paddingRight, - playerBinding.topBar.paddingBottom - ) - playerBinding.fullscreen.isInvisible = true playerBinding.closeImageButton.setOnClickListener { finish() } binding.player.initialize( - null, binding.doubleTapOverlay.binding, binding.playerGestureControlsView.binding, - trackSelector, ) } @@ -181,6 +165,7 @@ class OfflinePlayerActivity : BaseActivity() { player.setMediaSource(mediaSource) } + videoUri != null -> player.setMediaItem( MediaItem.Builder() .setUri(videoUri) @@ -189,6 +174,7 @@ class OfflinePlayerActivity : BaseActivity() { } .build(), ) + audioUri != null -> player.setMediaItem( MediaItem.Builder() .setUri(audioUri) 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 9406a7e79..c4fe65a6b 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 @@ -16,7 +16,6 @@ import android.os.PowerManager import android.text.format.DateUtils import android.text.method.LinkMovementMethod import android.text.util.Linkify -import android.util.Base64 import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -76,8 +75,6 @@ import com.github.libretube.extensions.toID import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.updateParameters import com.github.libretube.helpers.BackgroundHelper -import com.github.libretube.helpers.DashHelper -import com.github.libretube.helpers.DisplayHelper import com.github.libretube.helpers.ImageHelper import com.github.libretube.helpers.LocaleHelper import com.github.libretube.helpers.NavigationHelper @@ -892,14 +889,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { @SuppressLint("SetTextI18n") private fun initializePlayerView() { // initialize the player view actions - binding.player.initialize( - this, - doubleTapOverlayBinding, - playerGestureControlsViewBinding, - trackSelector, - viewModel, - viewLifecycleOwner, - ) + binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding) + binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this) binding.apply { val views = streams.views.formatShort() 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 542a91230..82a150ce3 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 @@ -17,16 +17,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.os.postDelayed import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.marginStart import androidx.core.view.updateLayoutParams -import androidx.lifecycle.LifecycleOwner import androidx.media3.common.Player import androidx.media3.common.text.Cue import androidx.media3.common.util.RepeatModeUtil import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.trackselection.TrackSelector import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.CaptionStyleCompat import androidx.media3.ui.PlayerView @@ -42,26 +39,23 @@ import com.github.libretube.extensions.round import com.github.libretube.helpers.AudioHelper import com.github.libretube.helpers.BrightnessHelper import com.github.libretube.helpers.PlayerHelper -import com.github.libretube.helpers.WindowHelper import com.github.libretube.obj.BottomSheetItem import com.github.libretube.ui.base.BaseActivity -import com.github.libretube.ui.extensions.toggleSystemBars -import com.github.libretube.ui.interfaces.OnlinePlayerOptions import com.github.libretube.ui.interfaces.PlayerGestureOptions import com.github.libretube.ui.interfaces.PlayerOptions import com.github.libretube.ui.listeners.PlayerGestureController -import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.sheets.BaseBottomSheet import com.github.libretube.ui.sheets.PlaybackOptionsSheet import com.github.libretube.util.PlayingQueue @SuppressLint("ClickableViewAccessibility") @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -internal class CustomExoPlayerView( +open class CustomExoPlayerView( context: Context, attributeSet: AttributeSet? = null, ) : PlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions { - val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this) + @Suppress("LeakingThis") + val binding = ExoStyledPlayerControlViewBinding.bind(this) /** * Objects for player tap and swipe gesture @@ -71,27 +65,20 @@ internal class CustomExoPlayerView( private lateinit var brightnessHelper: BrightnessHelper private lateinit var audioHelper: AudioHelper private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null - private var playerViewModel: PlayerViewModel? = null /** * Objects from the parent fragment */ - private var playerOptionsInterface: OnlinePlayerOptions? = null - private var trackSelector: TrackSelector? = null private val runnableHandler = Handler(Looper.getMainLooper()) - var isPlayerLocked: Boolean = false /** * Preferences */ - var autoplayEnabled = PlayerHelper.autoPlayEnabled - private var resizeModePref = PlayerHelper.resizeModePref - private val activity - get() = context as BaseActivity + val activity get() = context as BaseActivity private val supportFragmentManager get() = activity.supportFragmentManager @@ -101,18 +88,11 @@ internal class CustomExoPlayerView( } fun initialize( - playerViewInterface: OnlinePlayerOptions?, doubleTapOverlayBinding: DoubleTapOverlayBinding, playerGestureControlsViewBinding: PlayerGestureControlsViewBinding, - trackSelector: TrackSelector?, - playerViewModel: PlayerViewModel? = null, - viewLifecycleOwner: LifecycleOwner? = null, ) { - this.playerOptionsInterface = playerViewInterface this.doubleTapOverlayBinding = doubleTapOverlayBinding - this.trackSelector = trackSelector this.gestureViewBinding = playerGestureControlsViewBinding - this.playerViewModel = playerViewModel this.playerGestureController = PlayerGestureController(context as BaseActivity, this) this.brightnessHelper = BrightnessHelper(context as Activity) this.audioHelper = AudioHelper(context) @@ -123,7 +103,7 @@ internal class CustomExoPlayerView( initRewindAndForward() applyCaptionsStyle() - initializeAdvancedOptions(context) + initializeAdvancedOptions() // don't let the player view hide its controls automatically controllerShowTimeoutMs = -1 @@ -148,12 +128,6 @@ internal class CustomExoPlayerView( isPlayerLocked = !isPlayerLocked } - binding.autoPlay.isChecked = autoplayEnabled - - binding.autoPlay.setOnCheckedChangeListener { _, isChecked -> - autoplayEnabled = isChecked - } - resizeMode = when (resizeModePref) { "fill" -> AspectRatioFrameLayout.RESIZE_MODE_FILL "zoom" -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM @@ -183,11 +157,7 @@ internal class CustomExoPlayerView( // keep screen on if the video is playing keepScreenOn = player.isPlaying == true - - if (player.playbackState == Player.STATE_ENDED && !autoplayEnabled) { - showController() - cancelHideControllerTask() - } + onPlayerEvent(player, events) } } }) @@ -206,26 +176,10 @@ internal class CustomExoPlayerView( enqueueHideControllerTask() } }) - - setControllerVisibilityListener( - ControllerVisibilityListener { visibility -> - playerViewModel?.isFullscreen?.value?.let { isFullscreen -> - if (!isFullscreen) return@let - // Show status bar only not navigation bar if the player controls are visible and hide it otherwise - activity.toggleSystemBars( - types = WindowInsetsCompat.Type.statusBars(), - showBars = visibility == View.VISIBLE, - ) - } - }, - ) - - playerViewModel?.isFullscreen?.observe(viewLifecycleOwner!!) { isFullscreen -> - WindowHelper.toggleFullscreen(activity, isFullscreen) - updateTopBarMargin() - } } + open fun onPlayerEvent(player: Player, playerEvents: Player.Events) = Unit + private fun updatePlayPauseButton() { binding.playPauseBTN.setImageResource( when { @@ -250,16 +204,6 @@ internal class CustomExoPlayerView( // remove the callback to hide the controller cancelHideControllerTask() super.hideController() - - // hide system bars if in fullscreen or offline player - if (playerViewModel != null) { - if (playerViewModel!!.isFullscreen.value == true) { - WindowHelper.toggleFullscreen(activity, true) - } - updateTopBarMargin() - } else { - activity.toggleSystemBars(WindowInsetsCompat.Type.systemBars(), false) - } } override fun showController() { @@ -268,11 +212,6 @@ internal class CustomExoPlayerView( // automatically hide the controller after 2 seconds enqueueHideControllerTask() super.showController() - - // show the system bars when in offline player - if (playerViewModel == null) { - activity.toggleSystemBars(WindowInsetsCompat.Type.statusBars(), true) - } } override fun onTouchEvent(event: MotionEvent) = false @@ -300,97 +239,56 @@ internal class CustomExoPlayerView( } } - private fun initializeAdvancedOptions(context: Context) { + private fun initializeAdvancedOptions() { binding.toggleOptions.setOnClickListener { - val items = mutableListOf( - BottomSheetItem( - context.getString(R.string.repeat_mode), - R.drawable.ic_repeat, - { - if (player?.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { - context.getString(R.string.repeat_mode_none) - } else { - context.getString(R.string.repeat_mode_current) - } - }, - ) { - onRepeatModeClicked() - }, - BottomSheetItem( - context.getString(R.string.player_resize_mode), - R.drawable.ic_aspect_ratio, - { - when (resizeMode) { - AspectRatioFrameLayout.RESIZE_MODE_FIT -> context.getString( - R.string.resize_mode_fit, - ) - AspectRatioFrameLayout.RESIZE_MODE_FILL -> context.getString( - R.string.resize_mode_fill, - ) - else -> context.getString(R.string.resize_mode_zoom) - } - }, - ) { - onResizeModeClicked() - }, - BottomSheetItem( - context.getString(R.string.playback_speed), - R.drawable.ic_speed, - { - "${player?.playbackParameters?.speed?.round(2)}x" - }, - ) { - onPlaybackSpeedClicked() - }, - ) - - playerOptionsInterface?.let { - items.addAll( - listOf( - BottomSheetItem( - context.getString(R.string.quality), - R.drawable.ic_hd, - { "${player?.videoSize?.height}p" }, - ) { - it.onQualityClicked() - }, - BottomSheetItem( - context.getString(R.string.audio_track), - R.drawable.ic_audio, - { - trackSelector?.parameters?.preferredAudioLanguages?.firstOrNull() - }, - ) { - it.onAudioStreamClicked() - }, - BottomSheetItem( - context.getString(R.string.captions), - R.drawable.ic_caption, - { - if (trackSelector != null && trackSelector!!.parameters.preferredTextLanguages.isNotEmpty()) { - trackSelector!!.parameters.preferredTextLanguages[0] - } else { - context.getString(R.string.none) - } - }, - ) { - it.onCaptionsClicked() - }, - BottomSheetItem( - context.getString(R.string.stats_for_nerds), - R.drawable.ic_info, - ) { - it.onStatsClicked() - }, - ), - ) - } - + val items = getOptionsMenuItems() val bottomSheetFragment = BaseBottomSheet().setItems(items, null) bottomSheetFragment.show(supportFragmentManager, null) } } + open fun getOptionsMenuItems(): List = listOf( + BottomSheetItem( + context.getString(R.string.repeat_mode), + R.drawable.ic_repeat, + { + if (player?.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { + context.getString(R.string.repeat_mode_none) + } else { + context.getString(R.string.repeat_mode_current) + } + }, + ) { + onRepeatModeClicked() + }, + BottomSheetItem( + context.getString(R.string.player_resize_mode), + R.drawable.ic_aspect_ratio, + { + when (resizeMode) { + AspectRatioFrameLayout.RESIZE_MODE_FIT -> context.getString( + R.string.resize_mode_fit, + ) + AspectRatioFrameLayout.RESIZE_MODE_FILL -> context.getString( + R.string.resize_mode_fill, + ) + else -> context.getString(R.string.resize_mode_zoom) + } + }, + ) { + onResizeModeClicked() + }, + BottomSheetItem( + context.getString(R.string.playback_speed), + R.drawable.ic_speed, + { + "${player?.playbackParameters?.speed?.round(2)}x" + }, + ) { + onPlaybackSpeedClicked() + }, + ) + // lock the player private fun lockPlayer(isLocked: Boolean) { // isLocked is the current (old) state of the player lock @@ -606,12 +504,14 @@ internal class CustomExoPlayerView( .show(supportFragmentManager) } + open fun isFullscreen() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // add a larger bottom margin to the time bar in landscape mode val offset = when { - playerViewModel?.isFullscreen?.value ?: (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) -> 20.dpToPx() + isFullscreen() -> 20.dpToPx() else -> 10.dpToPx() } @@ -656,17 +556,16 @@ internal class CustomExoPlayerView( /** * Add extra margin to the top bar to not overlap the status bar */ - private fun updateTopBarMargin() { - val margin = when { - resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE -> 10 - playerViewModel?.isFullscreen?.value == true -> 20 - else -> 0 - } + fun updateTopBarMargin() { binding.topBar.updateLayoutParams { - topMargin = margin.dpToPx().toInt() + topMargin = getTopBarMarginDp().dpToPx().toInt() } } + open fun getTopBarMarginDp(): Int { + return if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) 10 else 0 + } + override fun onSingleTap() { toggleController() } @@ -719,7 +618,6 @@ internal class CustomExoPlayerView( playerGestureController.isMoving = false (context as? AppCompatActivity)?.onBackPressedDispatcher?.onBackPressed() - playerViewModel?.isFullscreen?.value = false } override fun onSwipeEnd() { diff --git a/app/src/main/java/com/github/libretube/ui/views/OfflinePlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/OfflinePlayerView.kt new file mode 100644 index 000000000..43fe90e59 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/views/OfflinePlayerView.kt @@ -0,0 +1,28 @@ +package com.github.libretube.ui.views + +import android.content.Context +import android.util.AttributeSet +import androidx.core.view.WindowInsetsCompat +import com.github.libretube.ui.extensions.toggleSystemBars + +class OfflinePlayerView( + context: Context, + attributeSet: AttributeSet? = null, +): CustomExoPlayerView(context, attributeSet) { + override fun hideController() { + super.hideController() + // hide the status bars when continuing to watch video + activity.toggleSystemBars(WindowInsetsCompat.Type.systemBars(), false) + } + + override fun showController() { + super.showController() + // show status bar when showing player options + activity.toggleSystemBars(WindowInsetsCompat.Type.statusBars(), true) + } + + override fun getTopBarMarginDp(): Int { + // the offline player requires a bigger top bar margin + return if (isFullscreen()) 18 else super.getTopBarMarginDp() + } +} 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 new file mode 100644 index 000000000..f7127b6dc --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt @@ -0,0 +1,132 @@ +package com.github.libretube.ui.views + +import android.content.Context +import android.content.res.Configuration +import android.util.AttributeSet +import android.view.View +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.LifecycleOwner +import androidx.media3.exoplayer.trackselection.TrackSelector +import com.github.libretube.R +import com.github.libretube.helpers.PlayerHelper +import com.github.libretube.helpers.WindowHelper +import com.github.libretube.obj.BottomSheetItem +import com.github.libretube.ui.extensions.toggleSystemBars +import com.github.libretube.ui.interfaces.OnlinePlayerOptions +import com.github.libretube.ui.models.PlayerViewModel + +class OnlinePlayerView( + context: Context, + attributeSet: AttributeSet? = null, +) : CustomExoPlayerView(context, attributeSet) { + private var playerOptions: OnlinePlayerOptions? = null + private var playerViewModel: PlayerViewModel? = null + private var trackSelector: TrackSelector? = null + private var viewLifecycleOwner: LifecycleOwner? = null + var autoplayEnabled = PlayerHelper.autoPlayEnabled + + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) + override fun getOptionsMenuItems(): List { + return super.getOptionsMenuItems() + + listOf( + BottomSheetItem( + context.getString(R.string.quality), + R.drawable.ic_hd, + { "${player?.videoSize?.height}p" }, + ) { + playerOptions?.onQualityClicked() + }, + BottomSheetItem( + context.getString(R.string.audio_track), + R.drawable.ic_audio, + { + trackSelector?.parameters?.preferredAudioLanguages?.firstOrNull() + }, + ) { + playerOptions?.onAudioStreamClicked() + }, + BottomSheetItem( + context.getString(R.string.captions), + R.drawable.ic_caption, + { + if (trackSelector != null && trackSelector!!.parameters.preferredTextLanguages.isNotEmpty()) { + trackSelector!!.parameters.preferredTextLanguages[0] + } else { + context.getString(R.string.none) + } + }, + ) { + playerOptions?.onCaptionsClicked() + }, + BottomSheetItem( + context.getString(R.string.stats_for_nerds), + R.drawable.ic_info, + ) { + playerOptions?.onStatsClicked() + }, + ) + + } + + fun initPlayerOptions( + playerViewModel: PlayerViewModel, + viewLifecycleOwner: LifecycleOwner, + trackSelector: TrackSelector, + playerOptions: OnlinePlayerOptions + ) { + this.playerViewModel = playerViewModel + this.viewLifecycleOwner = viewLifecycleOwner + this.trackSelector = trackSelector + this.playerOptions = playerOptions + + playerViewModel.isFullscreen.observe(viewLifecycleOwner) { isFullscreen -> + WindowHelper.toggleFullscreen(activity, isFullscreen) + updateTopBarMargin() + } + + setControllerVisibilityListener( + ControllerVisibilityListener { visibility -> + playerViewModel.isFullscreen.value?.let { isFullscreen -> + if (!isFullscreen) return@let + // Show status bar only not navigation bar if the player controls are visible and hide it otherwise + activity.toggleSystemBars( + types = WindowInsetsCompat.Type.statusBars(), + showBars = visibility == View.VISIBLE, + ) + } + }, + ) + + binding.autoPlay.isChecked = autoplayEnabled + + binding.autoPlay.setOnCheckedChangeListener { _, isChecked -> + autoplayEnabled = isChecked + } + } + + override fun hideController() { + super.hideController() + + if (playerViewModel?.isFullscreen?.value == true) { + WindowHelper.toggleFullscreen(activity, true) + } + updateTopBarMargin() + } + + override fun onSwipeCenterScreen(distanceY: Float) { + super.onSwipeCenterScreen(distanceY) + playerViewModel?.isFullscreen?.value = false + } + + override fun getTopBarMarginDp(): Int { + return when { + resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE -> 15 + playerViewModel?.isFullscreen?.value == true -> 20 + else -> super.getTopBarMarginDp() + } + } + + override fun isFullscreen(): Boolean { + return playerViewModel?.isFullscreen?.value ?: super.isFullscreen() + } +} diff --git a/app/src/main/res/layout/activity_offline_player.xml b/app/src/main/res/layout/activity_offline_player.xml index 9a552814f..5e4f886c2 100644 --- a/app/src/main/res/layout/activity_offline_player.xml +++ b/app/src/main/res/layout/activity_offline_player.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent" android:orientation="vertical"> - - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_player.xml b/app/src/main/res/layout/fragment_player.xml index c2638dd26..62393377c 100644 --- a/app/src/main/res/layout/fragment_player.xml +++ b/app/src/main/res/layout/fragment_player.xml @@ -299,7 +299,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - +