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
*/
const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
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.IntentData
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.obj.WatchPosition
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.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
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)
*/
private var player: ExoPlayer? = null
var player: ExoPlayer? = null
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()
}
@ -245,6 +237,7 @@ class BackgroundMode : Service() {
.setAudioAttributes(PlayerHelper.getAudioAttributes(), true)
.setLoadControl(PlayerHelper.getLoadControl())
.build()
.loadPlaybackParams()
/**
* 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
window.navigationBarColor = if (binding.bottomNav.menu.size() > 0) {
SurfaceColors.getColorForElevation(this, 10F)
SurfaceColors.getColorForElevation(this, binding.bottomNav.elevation)
} else {
ThemeHelper.getThemeColor(this, android.R.attr.colorBackground)
}
@ -120,7 +120,7 @@ class MainActivity : BaseActivity() {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment?
// get the current fragment
val fragment = navHostFragment?.childFragmentManager?.fragments?.get(0)
val fragment = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
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.models.PlayerViewModel
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.WindowHelper
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
@ -81,6 +82,7 @@ class OfflinePlayerActivity : BaseActivity() {
}
})
}
.loadPlaybackParams()
playerView = binding.player
playerView.setShowSubtitleButton(true)

View File

@ -7,6 +7,7 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.QueueRowBinding
import com.github.libretube.extensions.toID
import com.github.libretube.ui.viewholders.PlayingQueueViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.PlayingQueue
@ -46,10 +47,16 @@ class PlayingQueueAdapter : RecyclerView.Adapter<PlayingQueueViewHolder>() {
)
root.setOnClickListener {
val oldIndex = PlayingQueue.currentIndex()
PlayingQueue.onQueueItemSelected(position)
notifyItemChanged(oldIndex)
notifyItemChanged(position)
val oldPosition = PlayingQueue.currentIndex()
// get the new position from the queue to work properly after reordering the queue
val newPosition = PlayingQueue.getStreams().indexOfFirst {
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
channelImage.visibility = View.GONE
thumbnailDuration.setFormattedDuration(streamItem.duration!!)
thumbnailDuration.setFormattedDuration(streamItem.duration!!, streamItem.isShort)
ImageHelper.loadImage(streamItem.thumbnail, thumbnail)
root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, streamItem.url, playlistId)

View File

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

View File

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

View File

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

View File

@ -4,13 +4,10 @@ import android.text.format.DateUtils
import android.widget.TextView
import com.github.libretube.R
fun TextView.setFormattedDuration(duration: Long) {
val text = if (duration < 0L) {
this.context.getString(R.string.live)
} else if (duration in 0L..60L) {
this.context.getString(R.string.yt_shorts)
} else {
DateUtils.formatElapsedTime(duration)
fun TextView.setFormattedDuration(duration: Long, isShort: Boolean?) {
this.text = when {
isShort == true -> context.getString(R.string.yt_shorts)
duration < 0L -> context.getString(R.string.live)
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.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.databinding.FragmentAudioPlayerBinding
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.toID
import com.github.libretube.obj.ShareData
import com.github.libretube.services.BackgroundMode
import com.github.libretube.ui.activities.MainActivity
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.util.BackgroundHelper
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
import com.github.libretube.util.PlayingQueue
@ -32,7 +38,7 @@ class AudioPlayerFragment : BaseFragment() {
private var handler = Handler(Looper.getMainLooper())
private var isPaused: Boolean = false
private lateinit var playerService: BackgroundMode
private var playerService: BackgroundMode? = null
/** Defines callbacks for service binding, passed to bindService() */
private val connection = object : ServiceConnection {
@ -86,16 +92,42 @@ class AudioPlayerFragment : BaseFragment() {
PlayingQueue.onQueueItemSelected(currentIndex + 1)
}
binding.thumbnail.setOnClickListener {
binding.openQueue.setOnClickListener {
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
PlayingQueue.addOnTrackChangedListener(onTrackChangeListener)
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
@ -121,10 +153,8 @@ class AudioPlayerFragment : BaseFragment() {
}
private fun initializeSeekBar() {
if (!this::playerService.isInitialized) return
binding.timeBar.addOnChangeListener { _, value, fromUser ->
if (fromUser) playerService.seekToPosition(value.toLong() * 1000)
if (fromUser) playerService?.seekToPosition(value.toLong() * 1000)
}
updateSeekBar()
}
@ -133,7 +163,7 @@ class AudioPlayerFragment : BaseFragment() {
* Update the position, duration and text views belonging to the seek bar
*/
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
if (duration <= 0) {
@ -142,7 +172,7 @@ class AudioPlayerFragment : BaseFragment() {
}
// 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
binding.duration.text = DateUtils.formatElapsedTime((duration / 1000).toLong())
@ -161,7 +191,7 @@ class AudioPlayerFragment : BaseFragment() {
}
private fun handleServiceConnection() {
playerService.onIsPlayingChanged = { isPlaying ->
playerService?.onIsPlayingChanged = { isPlaying ->
binding.playPause.setIconResource(
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play
)
@ -172,7 +202,7 @@ class AudioPlayerFragment : BaseFragment() {
override fun onDestroy() {
// unregister all listeners and the connected [playerService]
playerService.onIsPlayingChanged = null
playerService?.onIsPlayingChanged = null
activity?.unbindService(connection)
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.NowPlayingNotification
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayerHelper.loadPlaybackParams
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.SeekbarPreviewListener
@ -1343,6 +1344,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
.setHandleAudioBecomingNoisy(true)
.setAudioAttributes(PlayerHelper.getAudioAttributes(), true)
.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.PlaybackParameters
class PlaybackSpeedSheet(
class PlaybackOptionsSheet(
private val player: ExoPlayer
) : ExpandedBottomSheet() {
private lateinit var binding: PlaybackBottomSheetBinding

View File

@ -16,7 +16,6 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
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.models.PlayerViewModel
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.BrightnessHelper
import com.github.libretube.util.PlayerGestureController
import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.text.Cue
import com.google.android.exoplayer2.trackselection.TrackSelector
@ -92,9 +89,6 @@ internal class CustomExoPlayerView(
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 {
hideController()
}
@ -127,17 +121,6 @@ internal class CustomExoPlayerView(
// don't let the player view hide its controls automatically
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
binding.lockPlayer.setOnClickListener {
// change the locked/unlocked icon
@ -218,9 +201,15 @@ internal class CustomExoPlayerView(
}
}
private fun cancelHideControllerTask() {
runCatching {
handler.removeCallbacks(hideControllerRunnable)
}
}
override fun hideController() {
// remove the callback to hide the controller
handler.removeCallbacks(hideControllerRunnable)
cancelHideControllerTask()
super.hideController()
// hide system bars if in fullscreen
@ -228,14 +217,15 @@ internal class CustomExoPlayerView(
if (it.isFullscreen.value == true) {
windowHelper?.setFullscreen()
}
updateTopBarMargin()
}
}
override fun showController() {
// remove the previous callback from the queue to prevent a flashing behavior
handler.removeCallbacks(hideControllerRunnable)
cancelHideControllerTask()
// automatically hide the controller after 2 seconds
handler.postDelayed(hideControllerRunnable, 2000)
handler.postDelayed(hideControllerRunnable, AUTO_HIDE_CONTROLLER_DELAY)
super.showController()
}
@ -392,8 +382,8 @@ internal class CustomExoPlayerView(
doubleTapOverlayBinding?.apply {
animateSeeking(rewindBTN, rewindIV, rewindTV, true)
runnableHandler.removeCallbacks(hideRewindButtonRunnable)
// start callback to hide the button
runnableHandler.removeCallbacks(hideRewindButtonRunnable)
runnableHandler.postDelayed(hideRewindButtonRunnable, 700)
}
}
@ -530,7 +520,7 @@ internal class CustomExoPlayerView(
override fun onPlaybackSpeedClicked() {
player?.let {
PlaybackSpeedSheet(it as ExoPlayer).show(supportFragmentManager)
PlaybackOptionsSheet(it as ExoPlayer).show(supportFragmentManager)
}
}
@ -591,15 +581,7 @@ internal class CustomExoPlayerView(
it.layoutParams = params
}
// add padding to the top bar to not overlap the status bar
binding.topBar.let {
setPadding(
it.paddingLeft,
(if (newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE) 25 else 5).toPixel().toInt(),
it.paddingRight,
it.paddingBottom
)
}
updateTopBarMargin()
// don't add extra padding if there's no cutout
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() {
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 {
private const val SUBTITLE_BOTTOM_PADDING_FRACTION = 0.158f
private const val ANIMATION_DURATION = 100L
private const val AUTO_HIDE_CONTROLLER_DELAY = 2000L
private val LANDSCAPE_MARGIN_HORIZONTAL = (20).toPixel().toInt()
}
}

View File

@ -60,11 +60,12 @@ object NavigationHelper {
playlistId: String? = null,
channelId: String? = null,
keepQueue: Boolean = false,
timeStamp: Long? = null
timeStamp: Long? = null,
forceVideo: Boolean = false
) {
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.playOnBackground(
context,

View File

@ -38,6 +38,7 @@ class NowPlayingNotification(
) {
private var videoId: String? = null
private var streams: Streams? = null
private var bitmap: Bitmap? = null
/**
* The [MediaSessionCompat] for the [streams].
@ -105,8 +106,17 @@ class NowPlayingNotification(
): Bitmap? {
if (DataSaverMode.isEnabled(context)) return null
var bitmap: Bitmap? = null
if (bitmap == null) enqueueThumbnailRequest(callback)
return bitmap
}
override fun getCurrentSubText(player: Player): CharSequence? {
return streams?.uploader
}
}
private fun enqueueThumbnailRequest(callback: PlayerNotificationManager.BitmapCallback) {
val request = ImageRequest.Builder(context)
.data(streams?.thumbnailUrl)
.target { result ->
@ -123,13 +133,6 @@ class NowPlayingNotification(
// enqueue the thumbnail loading request
ImageHelper.imageLoader.enqueue(request)
return bitmap
}
override fun getCurrentSubText(player: Player): CharSequence? {
return streams?.uploader
}
}
private val customActionReceiver = object : CustomActionReceiver {
@ -257,6 +260,8 @@ class NowPlayingNotification(
) {
this.videoId = videoId
this.streams = streams
// reset the thumbnail bitmap in order to become reloaded for the new video
this.bitmap = null
if (playerNotification == null) {
createMediaSession()

View File

@ -18,7 +18,9 @@ import com.github.libretube.enums.AudioQuality
import com.github.libretube.enums.PlayerEvent
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.LoadControl
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.video.VideoSize
@ -342,6 +344,12 @@ object PlayerHelper {
false
)
private val skipSilence: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.SKIP_SILENCE,
false
)
fun getDefaultResolution(context: Context): String {
return if (NetworkHelper.isNetworkMobile(context)) {
PreferenceHelper.getString(
@ -459,4 +467,16 @@ object PlayerHelper {
)
.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.view.WindowManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
@ -48,10 +49,6 @@ class WindowHelper(private val activity: BaseActivity) {
}
fun hasCutout(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
activity.window.decorView.rootWindowInsets.displayCutout != null
} else {
return false
}
return ViewCompat.getRootWindowInsets(activity.window.decorView)?.displayCutout != null
}
}

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_height="wrap_content"
android:layout_gravity="center"
android:textSize="18sp"
android:layout_marginTop="10dp" />
android:layout_marginTop="10dp"
android:textSize="18sp" />
</LinearLayout>
@ -56,8 +56,8 @@
android:id="@+id/time_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:labelBehavior="gone"
android:layout_marginHorizontal="20dp" />
android:layout_marginHorizontal="20dp"
app:labelBehavior="gone" />
<FrameLayout
android:layout_width="match_parent"
@ -84,7 +84,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginVertical="50dp">
android:layout_marginTop="24dp"
android:layout_marginBottom="36dp">
<ImageView
android:id="@+id/prev"
@ -118,4 +119,44 @@
</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>

View File

@ -238,4 +238,12 @@
</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>

View File

@ -78,17 +78,4 @@
</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>