Merge branch 'master' into KotlinX_Serialization

# Conflicts:
#	app/src/main/java/com/github/libretube/ui/adapters/SearchAdapter.kt
This commit is contained in:
Isira Seneviratne 2023-01-21 17:24:37 +05:30
commit 56763ec1a9
22 changed files with 225 additions and 118 deletions

View File

@ -93,7 +93,6 @@ object PreferenceKeys {
/** /**
* Background mode * Background mode
*/ */
const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
const val AUDIO_ONLY_MODE = "audio_only_mode" const val AUDIO_ONLY_MODE = "audio_only_mode"
/** /**

View File

@ -22,7 +22,6 @@ import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Companion.Database import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.WatchPosition import com.github.libretube.db.obj.WatchPosition
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
@ -32,8 +31,8 @@ import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toStreamItem import com.github.libretube.extensions.toStreamItem
import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.PlaybackException
@ -66,7 +65,7 @@ class BackgroundMode : Service() {
/** /**
* The [ExoPlayer] player. Followed tutorial [here](https://developer.android.com/codelabs/exoplayer-intro) * The [ExoPlayer] player. Followed tutorial [here](https://developer.android.com/codelabs/exoplayer-intro)
*/ */
private var player: ExoPlayer? = null var player: ExoPlayer? = null
private var playWhenReadyPlayer = true private var playWhenReadyPlayer = true
/** /**
@ -224,13 +223,6 @@ class BackgroundMode : Service() {
} }
} }
// set the playback speed
val playbackSpeed = PreferenceHelper.getString(
PreferenceKeys.BACKGROUND_PLAYBACK_SPEED,
"1"
).toFloat()
player?.setPlaybackSpeed(playbackSpeed)
fetchSponsorBlockSegments() fetchSponsorBlockSegments()
} }
@ -245,6 +237,7 @@ class BackgroundMode : Service() {
.setAudioAttributes(PlayerHelper.getAudioAttributes(), true) .setAudioAttributes(PlayerHelper.getAudioAttributes(), true)
.setLoadControl(PlayerHelper.getLoadControl()) .setLoadControl(PlayerHelper.getLoadControl())
.build() .build()
.loadPlaybackParams()
/** /**
* Listens for changed playbackStates (e.g. pause, end) * Listens for changed playbackStates (e.g. pause, end)

View File

@ -97,7 +97,7 @@ class MainActivity : BaseActivity() {
// sets the navigation bar color to the previously calculated color // sets the navigation bar color to the previously calculated color
window.navigationBarColor = if (binding.bottomNav.menu.size() > 0) { window.navigationBarColor = if (binding.bottomNav.menu.size() > 0) {
SurfaceColors.getColorForElevation(this, 10F) SurfaceColors.getColorForElevation(this, binding.bottomNav.elevation)
} else { } else {
ThemeHelper.getThemeColor(this, android.R.attr.colorBackground) ThemeHelper.getThemeColor(this, android.R.attr.colorBackground)
} }
@ -120,7 +120,7 @@ class MainActivity : BaseActivity() {
val navHostFragment = val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment? supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment?
// get the current fragment // get the current fragment
val fragment = navHostFragment?.childFragmentManager?.fragments?.get(0) val fragment = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
tryScrollToTop(fragment?.requireView() as? ViewGroup) tryScrollToTop(fragment?.requireView() as? ViewGroup)
} }
} }

View File

@ -20,6 +20,7 @@ import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setAspectRatio import com.github.libretube.ui.extensions.setAspectRatio
import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.WindowHelper import com.github.libretube.util.WindowHelper
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
@ -81,6 +82,7 @@ class OfflinePlayerActivity : BaseActivity() {
} }
}) })
} }
.loadPlaybackParams()
playerView = binding.player playerView = binding.player
playerView.setShowSubtitleButton(true) playerView.setShowSubtitleButton(true)

View File

@ -7,6 +7,7 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.QueueRowBinding import com.github.libretube.databinding.QueueRowBinding
import com.github.libretube.extensions.toID
import com.github.libretube.ui.viewholders.PlayingQueueViewHolder import com.github.libretube.ui.viewholders.PlayingQueueViewHolder
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
@ -46,10 +47,16 @@ class PlayingQueueAdapter : RecyclerView.Adapter<PlayingQueueViewHolder>() {
) )
root.setOnClickListener { root.setOnClickListener {
val oldIndex = PlayingQueue.currentIndex() val oldPosition = PlayingQueue.currentIndex()
PlayingQueue.onQueueItemSelected(position) // get the new position from the queue to work properly after reordering the queue
notifyItemChanged(oldIndex) val newPosition = PlayingQueue.getStreams().indexOfFirst {
notifyItemChanged(position) it.url?.toID() == streamItem.url?.toID()
}.takeIf { it >= 0 } ?: return@setOnClickListener
// select the new item in the queue and update the selected item in the UI
PlayingQueue.onQueueItemSelected(newPosition)
notifyItemChanged(oldPosition)
notifyItemChanged(newPosition)
} }
} }
} }

View File

@ -66,7 +66,7 @@ class PlaylistAdapter(
videoInfo.text = streamItem.uploaderName videoInfo.text = streamItem.uploaderName
channelImage.visibility = View.GONE channelImage.visibility = View.GONE
thumbnailDuration.setFormattedDuration(streamItem.duration!!) thumbnailDuration.setFormattedDuration(streamItem.duration!!, streamItem.isShort)
ImageHelper.loadImage(streamItem.thumbnail, thumbnail) ImageHelper.loadImage(streamItem.thumbnail, thumbnail)
root.setOnClickListener { root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId) NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId)

View File

@ -83,7 +83,7 @@ class SearchAdapter(
private fun bindWatch(item: ContentItem, binding: VideoRowBinding) { private fun bindWatch(item: ContentItem, binding: VideoRowBinding) {
binding.apply { binding.apply {
ImageHelper.loadImage(item.thumbnail, thumbnail) ImageHelper.loadImage(item.thumbnail, thumbnail)
thumbnailDuration.setFormattedDuration(item.duration) thumbnailDuration.setFormattedDuration(item.duration, item.isShort)
ImageHelper.loadImage(item.uploaderAvatar, channelImage) ImageHelper.loadImage(item.uploaderAvatar, channelImage)
videoTitle.text = item.title videoTitle.text = item.title
val viewsString = if (item.views != -1L) item.views.formatShort() else "" val viewsString = if (item.views != -1L) item.views.formatShort() else ""

View File

@ -133,7 +133,7 @@ class VideosAdapter(
TextUtils.SEPARATOR + video.uploaded?.let { TextUtils.SEPARATOR + video.uploaded?.let {
DateUtils.getRelativeTimeSpanString(it) DateUtils.getRelativeTimeSpanString(it)
} }
video.duration?.let { thumbnailDuration.setFormattedDuration(it) } video.duration?.let { thumbnailDuration.setFormattedDuration(it, video.isShort) }
channelImage.setOnClickListener { channelImage.setOnClickListener {
NavigationHelper.navigateChannel(root.context, video.uploaderUrl) NavigationHelper.navigateChannel(root.context, video.uploaderUrl)
} }

View File

@ -43,7 +43,7 @@ class WatchHistoryAdapter(
videoTitle.text = video.title videoTitle.text = video.title
channelName.text = video.uploader channelName.text = video.uploader
videoInfo.text = video.uploadDate videoInfo.text = video.uploadDate
thumbnailDuration.setFormattedDuration(video.duration!!) thumbnailDuration.setFormattedDuration(video.duration!!, null)
ImageHelper.loadImage(video.thumbnailUrl, thumbnail) ImageHelper.loadImage(video.thumbnailUrl, thumbnail)
ImageHelper.loadImage(video.uploaderAvatar, channelImage) ImageHelper.loadImage(video.uploaderAvatar, channelImage)

View File

@ -4,13 +4,10 @@ import android.text.format.DateUtils
import android.widget.TextView import android.widget.TextView
import com.github.libretube.R import com.github.libretube.R
fun TextView.setFormattedDuration(duration: Long) { fun TextView.setFormattedDuration(duration: Long, isShort: Boolean?) {
val text = if (duration < 0L) { this.text = when {
this.context.getString(R.string.live) isShort == true -> context.getString(R.string.yt_shorts)
} else if (duration in 0L..60L) { duration < 0L -> context.getString(R.string.live)
this.context.getString(R.string.yt_shorts) else -> DateUtils.formatElapsedTime(duration)
} else {
DateUtils.formatElapsedTime(duration)
} }
this.text = text
} }

View File

@ -12,14 +12,20 @@ import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.databinding.FragmentAudioPlayerBinding import com.github.libretube.databinding.FragmentAudioPlayerBinding
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.obj.ShareData
import com.github.libretube.services.BackgroundMode import com.github.libretube.services.BackgroundMode
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.base.BaseFragment import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
import com.github.libretube.ui.sheets.PlayingQueueSheet import com.github.libretube.ui.sheets.PlayingQueueSheet
import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
@ -32,7 +38,7 @@ class AudioPlayerFragment : BaseFragment() {
private var handler = Handler(Looper.getMainLooper()) private var handler = Handler(Looper.getMainLooper())
private var isPaused: Boolean = false private var isPaused: Boolean = false
private lateinit var playerService: BackgroundMode private var playerService: BackgroundMode? = null
/** Defines callbacks for service binding, passed to bindService() */ /** Defines callbacks for service binding, passed to bindService() */
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
@ -86,16 +92,42 @@ class AudioPlayerFragment : BaseFragment() {
PlayingQueue.onQueueItemSelected(currentIndex + 1) PlayingQueue.onQueueItemSelected(currentIndex + 1)
} }
binding.thumbnail.setOnClickListener { binding.openQueue.setOnClickListener {
PlayingQueueSheet().show(childFragmentManager) PlayingQueueSheet().show(childFragmentManager)
} }
binding.playbackOptions.setOnClickListener {
playerService?.player?.let {
PlaybackOptionsSheet(it)
.show(childFragmentManager)
}
}
binding.openVideo.setOnClickListener {
NavigationHelper.navigateVideo(
context = requireContext(),
videoId = PlayingQueue.getCurrent()?.url?.toID(),
keepQueue = true,
forceVideo = true
)
BackgroundHelper.stopBackgroundPlay(requireContext())
findNavController().popBackStack()
}
binding.share.setOnClickListener {
val currentVideo = PlayingQueue.getCurrent() ?: return@setOnClickListener
ShareDialog(
id = currentVideo.url!!.toID(),
shareObjectType = ShareObjectType.VIDEO,
shareData = ShareData(currentVideo = currentVideo.title)
).show(childFragmentManager, null)
}
// Listen for track changes due to autoplay or the notification // Listen for track changes due to autoplay or the notification
PlayingQueue.addOnTrackChangedListener(onTrackChangeListener) PlayingQueue.addOnTrackChangedListener(onTrackChangeListener)
binding.playPause.setOnClickListener { binding.playPause.setOnClickListener {
if (!this::playerService.isInitialized) return@setOnClickListener if (isPaused) playerService?.play() else playerService?.pause()
if (isPaused) playerService.play() else playerService.pause()
} }
// load the stream info into the UI // load the stream info into the UI
@ -121,10 +153,8 @@ class AudioPlayerFragment : BaseFragment() {
} }
private fun initializeSeekBar() { private fun initializeSeekBar() {
if (!this::playerService.isInitialized) return
binding.timeBar.addOnChangeListener { _, value, fromUser -> binding.timeBar.addOnChangeListener { _, value, fromUser ->
if (fromUser) playerService.seekToPosition(value.toLong() * 1000) if (fromUser) playerService?.seekToPosition(value.toLong() * 1000)
} }
updateSeekBar() updateSeekBar()
} }
@ -133,7 +163,7 @@ class AudioPlayerFragment : BaseFragment() {
* Update the position, duration and text views belonging to the seek bar * Update the position, duration and text views belonging to the seek bar
*/ */
private fun updateSeekBar() { private fun updateSeekBar() {
val duration = playerService.getDuration()?.toFloat() ?: return val duration = playerService?.getDuration()?.toFloat() ?: return
// when the video is not loaded yet, retry in 100 ms // when the video is not loaded yet, retry in 100 ms
if (duration <= 0) { if (duration <= 0) {
@ -142,7 +172,7 @@ class AudioPlayerFragment : BaseFragment() {
} }
// get the current position from the player service // get the current position from the player service
val currentPosition = playerService.getCurrentPosition()?.toFloat() ?: 0f val currentPosition = playerService?.getCurrentPosition()?.toFloat() ?: 0f
// set the text for the indicators // set the text for the indicators
binding.duration.text = DateUtils.formatElapsedTime((duration / 1000).toLong()) binding.duration.text = DateUtils.formatElapsedTime((duration / 1000).toLong())
@ -161,7 +191,7 @@ class AudioPlayerFragment : BaseFragment() {
} }
private fun handleServiceConnection() { private fun handleServiceConnection() {
playerService.onIsPlayingChanged = { isPlaying -> playerService?.onIsPlayingChanged = { isPlaying ->
binding.playPause.setIconResource( binding.playPause.setIconResource(
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play
) )
@ -172,7 +202,7 @@ class AudioPlayerFragment : BaseFragment() {
override fun onDestroy() { override fun onDestroy() {
// unregister all listeners and the connected [playerService] // unregister all listeners and the connected [playerService]
playerService.onIsPlayingChanged = null playerService?.onIsPlayingChanged = null
activity?.unbindService(connection) activity?.unbindService(connection)
PlayingQueue.removeOnTrackChangedListener(onTrackChangeListener) PlayingQueue.removeOnTrackChangedListener(onTrackChangeListener)

View File

@ -94,6 +94,7 @@ import com.github.libretube.util.LinkHandler
import com.github.libretube.util.NavigationHelper import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.SeekbarPreviewListener import com.github.libretube.util.SeekbarPreviewListener
@ -1343,6 +1344,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
.setHandleAudioBecomingNoisy(true) .setHandleAudioBecomingNoisy(true)
.setAudioAttributes(PlayerHelper.getAudioAttributes(), true) .setAudioAttributes(PlayerHelper.getAudioAttributes(), true)
.build() .build()
.loadPlaybackParams()
} }
/** /**

View File

@ -11,7 +11,7 @@ import com.github.libretube.util.PreferenceHelper
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackParameters import com.google.android.exoplayer2.PlaybackParameters
class PlaybackSpeedSheet( class PlaybackOptionsSheet(
private val player: ExoPlayer private val player: ExoPlayer
) : ExpandedBottomSheet() { ) : ExpandedBottomSheet() {
private lateinit var binding: PlaybackBottomSheetBinding private lateinit var binding: PlaybackBottomSheetBinding

View File

@ -16,7 +16,6 @@ import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.DoubleTapOverlayBinding import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.databinding.PlayerGestureControlsViewBinding import com.github.libretube.databinding.PlayerGestureControlsViewBinding
@ -30,15 +29,13 @@ import com.github.libretube.ui.interfaces.PlayerGestureOptions
import com.github.libretube.ui.interfaces.PlayerOptions import com.github.libretube.ui.interfaces.PlayerOptions
import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.PlaybackSpeedSheet import com.github.libretube.ui.sheets.PlaybackOptionsSheet
import com.github.libretube.util.AudioHelper import com.github.libretube.util.AudioHelper
import com.github.libretube.util.BrightnessHelper import com.github.libretube.util.BrightnessHelper
import com.github.libretube.util.PlayerGestureController import com.github.libretube.util.PlayerGestureController
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.text.Cue import com.google.android.exoplayer2.text.Cue
import com.google.android.exoplayer2.trackselection.TrackSelector import com.google.android.exoplayer2.trackselection.TrackSelector
@ -92,9 +89,6 @@ internal class CustomExoPlayerView(
if (isControllerFullyVisible) hideController() else showController() if (isControllerFullyVisible) hideController() else showController()
} }
// saved to only load the playback speed once (for the first video)
private var playbackPrefSet = false
private val hideControllerRunnable = Runnable { private val hideControllerRunnable = Runnable {
hideController() hideController()
} }
@ -127,17 +121,6 @@ internal class CustomExoPlayerView(
// don't let the player view hide its controls automatically // don't let the player view hide its controls automatically
controllerShowTimeoutMs = -1 controllerShowTimeoutMs = -1
if (!playbackPrefSet) {
player?.playbackParameters = PlaybackParameters(
PlayerHelper.playbackSpeed.toFloat(),
1.0f
)
PreferenceHelper.getBoolean(PreferenceKeys.SKIP_SILENCE, false).let {
(player as ExoPlayer).skipSilenceEnabled = it
}
playbackPrefSet = true
}
// locking the player // locking the player
binding.lockPlayer.setOnClickListener { binding.lockPlayer.setOnClickListener {
// change the locked/unlocked icon // change the locked/unlocked icon
@ -218,9 +201,15 @@ internal class CustomExoPlayerView(
} }
} }
private fun cancelHideControllerTask() {
runCatching {
handler.removeCallbacks(hideControllerRunnable)
}
}
override fun hideController() { override fun hideController() {
// remove the callback to hide the controller // remove the callback to hide the controller
handler.removeCallbacks(hideControllerRunnable) cancelHideControllerTask()
super.hideController() super.hideController()
// hide system bars if in fullscreen // hide system bars if in fullscreen
@ -228,14 +217,15 @@ internal class CustomExoPlayerView(
if (it.isFullscreen.value == true) { if (it.isFullscreen.value == true) {
windowHelper?.setFullscreen() windowHelper?.setFullscreen()
} }
updateTopBarMargin()
} }
} }
override fun showController() { override fun showController() {
// remove the previous callback from the queue to prevent a flashing behavior // remove the previous callback from the queue to prevent a flashing behavior
handler.removeCallbacks(hideControllerRunnable) cancelHideControllerTask()
// automatically hide the controller after 2 seconds // automatically hide the controller after 2 seconds
handler.postDelayed(hideControllerRunnable, 2000) handler.postDelayed(hideControllerRunnable, AUTO_HIDE_CONTROLLER_DELAY)
super.showController() super.showController()
} }
@ -392,8 +382,8 @@ internal class CustomExoPlayerView(
doubleTapOverlayBinding?.apply { doubleTapOverlayBinding?.apply {
animateSeeking(rewindBTN, rewindIV, rewindTV, true) animateSeeking(rewindBTN, rewindIV, rewindTV, true)
runnableHandler.removeCallbacks(hideRewindButtonRunnable)
// start callback to hide the button // start callback to hide the button
runnableHandler.removeCallbacks(hideRewindButtonRunnable)
runnableHandler.postDelayed(hideRewindButtonRunnable, 700) runnableHandler.postDelayed(hideRewindButtonRunnable, 700)
} }
} }
@ -530,7 +520,7 @@ internal class CustomExoPlayerView(
override fun onPlaybackSpeedClicked() { override fun onPlaybackSpeedClicked() {
player?.let { player?.let {
PlaybackSpeedSheet(it as ExoPlayer).show(supportFragmentManager) PlaybackOptionsSheet(it as ExoPlayer).show(supportFragmentManager)
} }
} }
@ -591,15 +581,7 @@ internal class CustomExoPlayerView(
it.layoutParams = params it.layoutParams = params
} }
// add padding to the top bar to not overlap the status bar updateTopBarMargin()
binding.topBar.let {
setPadding(
it.paddingLeft,
(if (newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE) 25 else 5).toPixel().toInt(),
it.paddingRight,
it.paddingBottom
)
}
// don't add extra padding if there's no cutout // don't add extra padding if there's no cutout
if ((context as? MainActivity)?.windowHelper?.hasCutout() == false) return if ((context as? MainActivity)?.windowHelper?.hasCutout() == false) return
@ -632,6 +614,19 @@ internal class CustomExoPlayerView(
} }
} }
/**
* Add extra margin to the top bar to not overlap the status bar
*/
private fun updateTopBarMargin() {
val isFullscreen = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE ||
playerViewModel?.isFullscreen?.value == true
binding.topBar.let {
it.layoutParams = (it.layoutParams as MarginLayoutParams).apply {
topMargin = (if (isFullscreen) 25 else 5).toPixel().toInt()
}
}
}
override fun onSingleTap() { override fun onSingleTap() {
toggleController() toggleController()
} }
@ -706,9 +701,22 @@ internal class CustomExoPlayerView(
} }
} }
/**
* Listen for all child touch events
*/
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
// when a control is clicked, restart the countdown to hide the controller
if (isControllerFullyVisible) {
cancelHideControllerTask()
handler.postDelayed(hideControllerRunnable, AUTO_HIDE_CONTROLLER_DELAY)
}
return super.onInterceptTouchEvent(ev)
}
companion object { companion object {
private const val SUBTITLE_BOTTOM_PADDING_FRACTION = 0.158f private const val SUBTITLE_BOTTOM_PADDING_FRACTION = 0.158f
private const val ANIMATION_DURATION = 100L private const val ANIMATION_DURATION = 100L
private const val AUTO_HIDE_CONTROLLER_DELAY = 2000L
private val LANDSCAPE_MARGIN_HORIZONTAL = (20).toPixel().toInt() private val LANDSCAPE_MARGIN_HORIZONTAL = (20).toPixel().toInt()
} }
} }

View File

@ -60,11 +60,12 @@ object NavigationHelper {
playlistId: String? = null, playlistId: String? = null,
channelId: String? = null, channelId: String? = null,
keepQueue: Boolean = false, keepQueue: Boolean = false,
timeStamp: Long? = null timeStamp: Long? = null,
forceVideo: Boolean = false
) { ) {
if (videoId == null) return if (videoId == null) return
if (PreferenceHelper.getBoolean(PreferenceKeys.AUDIO_ONLY_MODE, false)) { if (PreferenceHelper.getBoolean(PreferenceKeys.AUDIO_ONLY_MODE, false) && !forceVideo) {
BackgroundHelper.stopBackgroundPlay(context) BackgroundHelper.stopBackgroundPlay(context)
BackgroundHelper.playOnBackground( BackgroundHelper.playOnBackground(
context, context,

View File

@ -38,6 +38,7 @@ class NowPlayingNotification(
) { ) {
private var videoId: String? = null private var videoId: String? = null
private var streams: Streams? = null private var streams: Streams? = null
private var bitmap: Bitmap? = null
/** /**
* The [MediaSessionCompat] for the [streams]. * The [MediaSessionCompat] for the [streams].
@ -105,24 +106,7 @@ class NowPlayingNotification(
): Bitmap? { ): Bitmap? {
if (DataSaverMode.isEnabled(context)) return null if (DataSaverMode.isEnabled(context)) return null
var bitmap: Bitmap? = null if (bitmap == null) enqueueThumbnailRequest(callback)
val request = ImageRequest.Builder(context)
.data(streams?.thumbnailUrl)
.target { result ->
val bm = (result as BitmapDrawable).bitmap
// returns the bitmap on Android 13+, for everything below scaled down to a square
bitmap = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
ImageHelper.getSquareBitmap(bm)
} else {
bm
}
callback.onBitmap(bitmap!!)
}
.build()
// enqueue the thumbnail loading request
ImageHelper.imageLoader.enqueue(request)
return bitmap return bitmap
} }
@ -132,6 +116,25 @@ class NowPlayingNotification(
} }
} }
private fun enqueueThumbnailRequest(callback: PlayerNotificationManager.BitmapCallback) {
val request = ImageRequest.Builder(context)
.data(streams?.thumbnailUrl)
.target { result ->
val bm = (result as BitmapDrawable).bitmap
// returns the bitmap on Android 13+, for everything below scaled down to a square
bitmap = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
ImageHelper.getSquareBitmap(bm)
} else {
bm
}
callback.onBitmap(bitmap!!)
}
.build()
// enqueue the thumbnail loading request
ImageHelper.imageLoader.enqueue(request)
}
private val customActionReceiver = object : CustomActionReceiver { private val customActionReceiver = object : CustomActionReceiver {
override fun createCustomActions( override fun createCustomActions(
context: Context, context: Context,
@ -257,6 +260,8 @@ class NowPlayingNotification(
) { ) {
this.videoId = videoId this.videoId = videoId
this.streams = streams this.streams = streams
// reset the thumbnail bitmap in order to become reloaded for the new video
this.bitmap = null
if (playerNotification == null) { if (playerNotification == null) {
createMediaSession() createMediaSession()

View File

@ -18,7 +18,9 @@ import com.github.libretube.enums.AudioQuality
import com.github.libretube.enums.PlayerEvent import com.github.libretube.enums.PlayerEvent
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.LoadControl import com.google.android.exoplayer2.LoadControl
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ui.CaptionStyleCompat import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.video.VideoSize import com.google.android.exoplayer2.video.VideoSize
@ -342,6 +344,12 @@ object PlayerHelper {
false false
) )
private val skipSilence: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.SKIP_SILENCE,
false
)
fun getDefaultResolution(context: Context): String { fun getDefaultResolution(context: Context): String {
return if (NetworkHelper.isNetworkMobile(context)) { return if (NetworkHelper.isNetworkMobile(context)) {
PreferenceHelper.getString( PreferenceHelper.getString(
@ -459,4 +467,16 @@ object PlayerHelper {
) )
.build() .build()
} }
/**
* Load playback parameters such as speed and skip silence
*/
fun ExoPlayer.loadPlaybackParams(): ExoPlayer {
skipSilenceEnabled = skipSilence
playbackParameters = PlaybackParameters(
playbackSpeed.toFloat(),
1.0f
)
return this
}
} }

View File

@ -2,6 +2,7 @@ package com.github.libretube.util
import android.os.Build import android.os.Build
import android.view.WindowManager import android.view.WindowManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
@ -48,10 +49,6 @@ class WindowHelper(private val activity: BaseActivity) {
} }
fun hasCutout(): Boolean { fun hasCutout(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return ViewCompat.getRootWindowInsets(activity.window.decorView)?.displayCutout != null
activity.window.decorView.rootWindowInsets.displayCutout != null
} else {
return false
}
} }
} }

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M21,3L3,3c-1.11,0 -2,0.89 -2,2v12c0,1.1 0.89,2 2,2h5v2h8v-2h5c1.1,0 1.99,-0.9 1.99,-2L23,5c0,-1.11 -0.9,-2 -2,-2zM21,17L3,17L3,5h18v12zM16,11l-7,4L9,7z" />
</vector>

View File

@ -47,8 +47,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:textSize="18sp" android:layout_marginTop="10dp"
android:layout_marginTop="10dp" /> android:textSize="18sp" />
</LinearLayout> </LinearLayout>
@ -56,8 +56,8 @@
android:id="@+id/time_bar" android:id="@+id/time_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:labelBehavior="gone" android:layout_marginHorizontal="20dp"
android:layout_marginHorizontal="20dp" /> app:labelBehavior="gone" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -69,14 +69,14 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start|center" android:layout_gravity="start|center"
tools:text="00:00"/> tools:text="00:00" />
<TextView <TextView
android:id="@+id/duration" android:id="@+id/duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|center" android:layout_gravity="end|center"
tools:text="10:15"/> tools:text="10:15" />
</FrameLayout> </FrameLayout>
@ -84,7 +84,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginVertical="50dp"> android:layout_marginTop="24dp"
android:layout_marginBottom="36dp">
<ImageView <ImageView
android:id="@+id/prev" android:id="@+id/prev"
@ -118,4 +119,44 @@
</LinearLayout> </LinearLayout>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:cardCornerRadius="18dp"
android:layout_marginBottom="30dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="15dp">
<ImageView
android:id="@+id/open_queue"
style="@style/AudioPlayerButton"
android:src="@drawable/ic_queue" />
<ImageView
android:id="@+id/playback_options"
style="@style/AudioPlayerButton"
android:layout_width="27dp"
android:layout_height="27dp"
android:src="@drawable/ic_speed" />
<ImageView
android:id="@+id/open_video"
style="@style/AudioPlayerButton"
android:src="@drawable/ic_video" />
<ImageView
android:id="@+id/share"
style="@style/AudioPlayerButton"
android:src="@drawable/ic_share" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>

View File

@ -238,4 +238,12 @@
</style> </style>
<style name="AudioPlayerButton">
<item name="android:layout_width">30dp</item>
<item name="android:layout_height">30dp</item>
<item name="android:background">?attr/selectableItemBackgroundBorderless</item>
<item name="android:layout_marginStart">10dp</item>
<item name="android:layout_marginEnd">10dp</item>
</style>
</resources> </resources>

View File

@ -78,17 +78,4 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/background_mode">
<com.github.libretube.ui.views.SliderPreference
android:icon="@drawable/ic_speed"
app:defValue="1.0"
app:key="background_playback_speed"
app:stepSize="0.1"
app:title="@string/playback_speed"
app:valueFrom="0.2"
app:valueTo="4.0" />
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>