mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 22:30:30 +05:30
commit
6ab3c96f87
@ -0,0 +1,7 @@
|
|||||||
|
package com.github.libretube.extensions
|
||||||
|
|
||||||
|
fun <T> MutableList<T>.move(oldPosition: Int, newPosition: Int) {
|
||||||
|
val item = this.get(oldPosition)
|
||||||
|
this.removeAt(oldPosition)
|
||||||
|
this.add(newPosition, item)
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.github.libretube.extensions
|
||||||
|
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
import com.github.libretube.api.obj.Streams
|
||||||
|
|
||||||
|
fun Streams.toStreamItem(videoId: String): StreamItem {
|
||||||
|
return StreamItem(
|
||||||
|
url = videoId,
|
||||||
|
title = title,
|
||||||
|
thumbnail = thumbnailUrl,
|
||||||
|
uploaderName = uploader,
|
||||||
|
uploaderUrl = uploaderUrl,
|
||||||
|
uploaderAvatar = uploaderAvatar,
|
||||||
|
uploadedDate = uploadDate,
|
||||||
|
uploaded = null,
|
||||||
|
duration = duration,
|
||||||
|
views = views,
|
||||||
|
uploaderVerified = uploaderVerified,
|
||||||
|
shortDescription = description
|
||||||
|
)
|
||||||
|
}
|
@ -24,8 +24,7 @@ import com.github.libretube.constants.PreferenceKeys
|
|||||||
import com.github.libretube.db.DatabaseHelper
|
import com.github.libretube.db.DatabaseHelper
|
||||||
import com.github.libretube.db.DatabaseHolder
|
import com.github.libretube.db.DatabaseHolder
|
||||||
import com.github.libretube.extensions.awaitQuery
|
import com.github.libretube.extensions.awaitQuery
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toStreamItem
|
||||||
import com.github.libretube.util.AutoPlayHelper
|
|
||||||
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.PlayingQueue
|
import com.github.libretube.util.PlayingQueue
|
||||||
@ -80,16 +79,6 @@ class BackgroundMode : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var nowPlayingNotification: NowPlayingNotification
|
private lateinit var nowPlayingNotification: NowPlayingNotification
|
||||||
|
|
||||||
/**
|
|
||||||
* The [videoId] of the next stream for autoplay
|
|
||||||
*/
|
|
||||||
private var nextStreamId: String? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper for finding the next video in the playlist
|
|
||||||
*/
|
|
||||||
private lateinit var autoPlayHelper: AutoPlayHelper
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autoplay Preference
|
* Autoplay Preference
|
||||||
*/
|
*/
|
||||||
@ -132,9 +121,6 @@ class BackgroundMode : Service() {
|
|||||||
playlistId = intent.getStringExtra(IntentData.playlistId)
|
playlistId = intent.getStringExtra(IntentData.playlistId)
|
||||||
val position = intent.getLongExtra(IntentData.position, 0L)
|
val position = intent.getLongExtra(IntentData.position, 0L)
|
||||||
|
|
||||||
// initialize the playlist autoPlay Helper
|
|
||||||
autoPlayHelper = AutoPlayHelper(playlistId)
|
|
||||||
|
|
||||||
// play the audio in the background
|
// play the audio in the background
|
||||||
loadAudio(videoId, position)
|
loadAudio(videoId, position)
|
||||||
|
|
||||||
@ -146,7 +132,9 @@ class BackgroundMode : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWatchPosition() {
|
private fun updateWatchPosition() {
|
||||||
player?.currentPosition?.let { DatabaseHelper.saveWatchPosition(videoId, it) }
|
player?.currentPosition?.let {
|
||||||
|
DatabaseHelper.saveWatchPosition(videoId, it)
|
||||||
|
}
|
||||||
handler.postDelayed(this::updateWatchPosition, 500)
|
handler.postDelayed(this::updateWatchPosition, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,8 +145,6 @@ class BackgroundMode : Service() {
|
|||||||
videoId: String,
|
videoId: String,
|
||||||
seekToPosition: Long = 0
|
seekToPosition: Long = 0
|
||||||
) {
|
) {
|
||||||
// append the video to the playing queue
|
|
||||||
PlayingQueue.add(videoId)
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
streams = RetrofitInstance.api.getStreams(videoId)
|
streams = RetrofitInstance.api.getStreams(videoId)
|
||||||
@ -166,6 +152,17 @@ class BackgroundMode : Service() {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add the playlist video to the queue
|
||||||
|
if (playlistId != null && PlayingQueue.isEmpty()) {
|
||||||
|
streams?.toStreamItem(videoId)
|
||||||
|
?.let { PlayingQueue.insertPlaylist(playlistId!!, it) }
|
||||||
|
} else {
|
||||||
|
streams?.toStreamItem(videoId)?.let { PlayingQueue.updateCurrent(it) }
|
||||||
|
streams?.relatedStreams?.toTypedArray()?.let {
|
||||||
|
PlayingQueue.add(*it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
playAudio(seekToPosition)
|
playAudio(seekToPosition)
|
||||||
}
|
}
|
||||||
@ -175,8 +172,6 @@ class BackgroundMode : Service() {
|
|||||||
private fun playAudio(
|
private fun playAudio(
|
||||||
seekToPosition: Long
|
seekToPosition: Long
|
||||||
) {
|
) {
|
||||||
PlayingQueue.updateCurrent(videoId)
|
|
||||||
|
|
||||||
initializePlayer()
|
initializePlayer()
|
||||||
setMediaItem()
|
setMediaItem()
|
||||||
|
|
||||||
@ -218,8 +213,6 @@ class BackgroundMode : Service() {
|
|||||||
player?.setPlaybackSpeed(playbackSpeed)
|
player?.setPlaybackSpeed(playbackSpeed)
|
||||||
|
|
||||||
fetchSponsorBlockSegments()
|
fetchSponsorBlockSegments()
|
||||||
|
|
||||||
if (PlayerHelper.autoPlayEnabled) setNextStream()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -268,32 +261,16 @@ class BackgroundMode : Service() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* set the videoId of the next stream for autoplay
|
|
||||||
*/
|
|
||||||
private fun setNextStream() {
|
|
||||||
if (streams!!.relatedStreams!!.isNotEmpty()) {
|
|
||||||
nextStreamId = streams?.relatedStreams!![0].url!!.toID()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playlistId == null) return
|
|
||||||
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
|
|
||||||
// search for the next videoId in the playlist
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
nextStreamId = autoPlayHelper.getNextVideoId(videoId, streams!!.relatedStreams!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays the first related video to the current (used when the playback of the current video ended)
|
* Plays the first related video to the current (used when the playback of the current video ended)
|
||||||
*/
|
*/
|
||||||
private fun playNextVideo() {
|
private fun playNextVideo() {
|
||||||
if (nextStreamId == null || nextStreamId == videoId) return
|
val nextVideo = PlayingQueue.getNext()
|
||||||
val nextQueueVideo = PlayingQueue.getNext()
|
|
||||||
if (nextQueueVideo != null) nextStreamId = nextQueueVideo
|
|
||||||
|
|
||||||
// play new video on background
|
// play new video on background
|
||||||
this.videoId = nextStreamId!!
|
if (nextVideo != null) {
|
||||||
|
this.videoId = nextVideo
|
||||||
|
}
|
||||||
this.segmentData = null
|
this.segmentData = null
|
||||||
loadAudio(videoId)
|
loadAudio(videoId)
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,10 @@ import com.github.libretube.services.ClosingService
|
|||||||
import com.github.libretube.ui.base.BaseActivity
|
import com.github.libretube.ui.base.BaseActivity
|
||||||
import com.github.libretube.ui.dialogs.ErrorDialog
|
import com.github.libretube.ui.dialogs.ErrorDialog
|
||||||
import com.github.libretube.ui.fragments.PlayerFragment
|
import com.github.libretube.ui.fragments.PlayerFragment
|
||||||
|
import com.github.libretube.ui.sheets.PlayingQueueSheet
|
||||||
import com.github.libretube.util.NavBarHelper
|
import com.github.libretube.util.NavBarHelper
|
||||||
import com.github.libretube.util.NetworkHelper
|
import com.github.libretube.util.NetworkHelper
|
||||||
|
import com.github.libretube.util.PlayingQueue
|
||||||
import com.github.libretube.util.PreferenceHelper
|
import com.github.libretube.util.PreferenceHelper
|
||||||
import com.github.libretube.util.ThemeHelper
|
import com.github.libretube.util.ThemeHelper
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
@ -195,6 +197,11 @@ class MainActivity : BaseActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menu?.findItem(R.id.action_queue)?.isVisible = PlayingQueue.isNotEmpty()
|
||||||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the notification badge showing the amount of new videos
|
* Initialize the notification badge showing the amount of new videos
|
||||||
*/
|
*/
|
||||||
@ -302,6 +309,10 @@ class MainActivity : BaseActivity() {
|
|||||||
startActivity(communityIntent)
|
startActivity(communityIntent)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_queue -> {
|
||||||
|
PlayingQueueSheet().show(supportFragmentManager, null)
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
package com.github.libretube.ui.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.github.libretube.databinding.QueueRowBinding
|
||||||
|
import com.github.libretube.ui.viewholders.PlayingQueueViewHolder
|
||||||
|
import com.github.libretube.util.ImageHelper
|
||||||
|
import com.github.libretube.util.PlayingQueue
|
||||||
|
import com.github.libretube.util.ThemeHelper
|
||||||
|
|
||||||
|
class PlayingQueueAdapter : RecyclerView.Adapter<PlayingQueueViewHolder>() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlayingQueueViewHolder {
|
||||||
|
val binding = QueueRowBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return PlayingQueueViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return PlayingQueue.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onBindViewHolder(holder: PlayingQueueViewHolder, position: Int) {
|
||||||
|
val streamItem = PlayingQueue.getStreams()[position]
|
||||||
|
holder.binding.apply {
|
||||||
|
ImageHelper.loadImage(streamItem.thumbnail, thumbnail)
|
||||||
|
title.text = streamItem.title
|
||||||
|
videoInfo.text = streamItem.uploaderName + " • " +
|
||||||
|
DateUtils.formatElapsedTime(streamItem.duration ?: 0)
|
||||||
|
|
||||||
|
if (PlayingQueue.currentIndex() == position) {
|
||||||
|
root.setBackgroundColor(
|
||||||
|
ThemeHelper.getThemeColor(root.context, android.R.attr.colorControlHighlight)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -52,6 +52,7 @@ import com.github.libretube.extensions.formatShort
|
|||||||
import com.github.libretube.extensions.hideKeyboard
|
import com.github.libretube.extensions.hideKeyboard
|
||||||
import com.github.libretube.extensions.query
|
import com.github.libretube.extensions.query
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
|
import com.github.libretube.extensions.toStreamItem
|
||||||
import com.github.libretube.models.PlayerViewModel
|
import com.github.libretube.models.PlayerViewModel
|
||||||
import com.github.libretube.models.interfaces.PlayerOptionsInterface
|
import com.github.libretube.models.interfaces.PlayerOptionsInterface
|
||||||
import com.github.libretube.services.BackgroundMode
|
import com.github.libretube.services.BackgroundMode
|
||||||
@ -64,8 +65,8 @@ import com.github.libretube.ui.base.BaseFragment
|
|||||||
import com.github.libretube.ui.dialogs.AddToPlaylistDialog
|
import com.github.libretube.ui.dialogs.AddToPlaylistDialog
|
||||||
import com.github.libretube.ui.dialogs.DownloadDialog
|
import com.github.libretube.ui.dialogs.DownloadDialog
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog
|
import com.github.libretube.ui.dialogs.ShareDialog
|
||||||
|
import com.github.libretube.ui.sheets.PlayingQueueSheet
|
||||||
import com.github.libretube.ui.views.BottomSheet
|
import com.github.libretube.ui.views.BottomSheet
|
||||||
import com.github.libretube.util.AutoPlayHelper
|
|
||||||
import com.github.libretube.util.BackgroundHelper
|
import com.github.libretube.util.BackgroundHelper
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
import com.github.libretube.util.NowPlayingNotification
|
import com.github.libretube.util.NowPlayingNotification
|
||||||
@ -153,12 +154,6 @@ class PlayerFragment : BaseFragment() {
|
|||||||
private var token = PreferenceHelper.getToken()
|
private var token = PreferenceHelper.getToken()
|
||||||
private var videoShownInExternalPlayer = false
|
private var videoShownInExternalPlayer = false
|
||||||
|
|
||||||
/**
|
|
||||||
* for autoplay
|
|
||||||
*/
|
|
||||||
private var nextStreamId: String? = null
|
|
||||||
private lateinit var autoPlayHelper: AutoPlayHelper
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* for the player notification
|
* for the player notification
|
||||||
*/
|
*/
|
||||||
@ -409,6 +404,11 @@ class PlayerFragment : BaseFragment() {
|
|||||||
toggleComments()
|
toggleComments()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playerBinding.queueToggle.visibility = View.VISIBLE
|
||||||
|
playerBinding.queueToggle.setOnClickListener {
|
||||||
|
PlayingQueueSheet().show(childFragmentManager, null)
|
||||||
|
}
|
||||||
|
|
||||||
// FullScreen button trigger
|
// FullScreen button trigger
|
||||||
// hide fullscreen button if auto rotation enabled
|
// hide fullscreen button if auto rotation enabled
|
||||||
playerBinding.fullscreen.visibility =
|
playerBinding.fullscreen.visibility =
|
||||||
@ -630,8 +630,6 @@ class PlayerFragment : BaseFragment() {
|
|||||||
|
|
||||||
private fun playVideo() {
|
private fun playVideo() {
|
||||||
lifecycleScope.launchWhenCreated {
|
lifecycleScope.launchWhenCreated {
|
||||||
PlayingQueue.updateCurrent(videoId!!)
|
|
||||||
|
|
||||||
streams = try {
|
streams = try {
|
||||||
RetrofitInstance.api.getStreams(videoId!!)
|
RetrofitInstance.api.getStreams(videoId!!)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
@ -645,6 +643,21 @@ class PlayerFragment : BaseFragment() {
|
|||||||
return@launchWhenCreated
|
return@launchWhenCreated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PlayingQueue.isEmpty()) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (playlistId != null) {
|
||||||
|
PlayingQueue.insertPlaylist(playlistId!!, streams.toStreamItem(videoId!!))
|
||||||
|
} else {
|
||||||
|
PlayingQueue.updateCurrent(streams.toStreamItem(videoId!!))
|
||||||
|
PlayingQueue.add(
|
||||||
|
*streams.relatedStreams.orEmpty().toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PlayingQueue.updateCurrent(streams.toStreamItem(videoId!!))
|
||||||
|
}
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
// hide the button to skip SponsorBlock segments manually
|
// hide the button to skip SponsorBlock segments manually
|
||||||
binding.sbSkipBtn.visibility = View.GONE
|
binding.sbSkipBtn.visibility = View.GONE
|
||||||
@ -668,8 +681,6 @@ class PlayerFragment : BaseFragment() {
|
|||||||
if (PlayerHelper.sponsorBlockEnabled) fetchSponsorBlockSegments()
|
if (PlayerHelper.sponsorBlockEnabled) fetchSponsorBlockSegments()
|
||||||
// show comments if related streams disabled
|
// show comments if related streams disabled
|
||||||
if (!PlayerHelper.relatedStreamsEnabled) toggleComments()
|
if (!PlayerHelper.relatedStreamsEnabled) toggleComments()
|
||||||
// prepare for autoplay
|
|
||||||
if (binding.player.autoplayEnabled) setNextStream()
|
|
||||||
|
|
||||||
// add the video to the watch history
|
// add the video to the watch history
|
||||||
if (PlayerHelper.watchHistoryEnabled) {
|
if (PlayerHelper.watchHistoryEnabled) {
|
||||||
@ -682,17 +693,6 @@ class PlayerFragment : BaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* set the videoId of the next stream for autoplay
|
|
||||||
*/
|
|
||||||
private fun setNextStream() {
|
|
||||||
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId)
|
|
||||||
// search for the next videoId in the playlist
|
|
||||||
lifecycleScope.launchWhenCreated {
|
|
||||||
nextStreamId = autoPlayHelper.getNextVideoId(videoId!!, streams.relatedStreams)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fetch the segments for SponsorBlock
|
* fetch the segments for SponsorBlock
|
||||||
*/
|
*/
|
||||||
@ -758,18 +758,19 @@ class PlayerFragment : BaseFragment() {
|
|||||||
|
|
||||||
// used for autoplay and skipping to next video
|
// used for autoplay and skipping to next video
|
||||||
private fun playNextVideo() {
|
private fun playNextVideo() {
|
||||||
if (nextStreamId == null) return
|
val nextVideoId = PlayingQueue.getNext()
|
||||||
// check whether there is a new video in the queue
|
|
||||||
val nextQueueVideo = PlayingQueue.getNext()
|
|
||||||
if (nextQueueVideo != null) nextStreamId = nextQueueVideo
|
|
||||||
// by making sure that the next and the current video aren't the same
|
// by making sure that the next and the current video aren't the same
|
||||||
saveWatchPosition()
|
saveWatchPosition()
|
||||||
// forces the comments to reload for the new video
|
|
||||||
commentsLoaded = false
|
|
||||||
binding.commentsRecView.adapter = null
|
|
||||||
// save the id of the next stream as videoId and load the next video
|
// save the id of the next stream as videoId and load the next video
|
||||||
videoId = nextStreamId
|
if (nextVideoId != null) {
|
||||||
playVideo()
|
videoId = nextVideoId
|
||||||
|
|
||||||
|
// forces the comments to reload for the new video
|
||||||
|
commentsLoaded = false
|
||||||
|
binding.commentsRecView.adapter = null
|
||||||
|
playVideo()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun prepareExoPlayerView() {
|
private fun prepareExoPlayerView() {
|
||||||
@ -866,7 +867,6 @@ class PlayerFragment : BaseFragment() {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (
|
if (
|
||||||
playbackState == Player.STATE_ENDED &&
|
playbackState == Player.STATE_ENDED &&
|
||||||
nextStreamId != null &&
|
|
||||||
!transitioning &&
|
!transitioning &&
|
||||||
binding.player.autoplayEnabled
|
binding.player.autoplayEnabled
|
||||||
) {
|
) {
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
package com.github.libretube.ui.sheets
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.github.libretube.databinding.BottomSheetBinding
|
||||||
|
import com.github.libretube.ui.adapters.PlayingQueueAdapter
|
||||||
|
import com.github.libretube.util.PlayingQueue
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
|
||||||
|
class PlayingQueueSheet : BottomSheetDialogFragment() {
|
||||||
|
private lateinit var binding: BottomSheetBinding
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = BottomSheetBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.optionsRecycler.layoutManager = LinearLayoutManager(context)
|
||||||
|
val adapter = PlayingQueueAdapter()
|
||||||
|
binding.optionsRecycler.adapter = adapter
|
||||||
|
|
||||||
|
val callback = object : ItemTouchHelper.SimpleCallback(
|
||||||
|
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
|
||||||
|
ItemTouchHelper.LEFT
|
||||||
|
) {
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
val from = viewHolder.absoluteAdapterPosition
|
||||||
|
val to = target.absoluteAdapterPosition
|
||||||
|
|
||||||
|
adapter.notifyItemMoved(from, to)
|
||||||
|
PlayingQueue.move(from, to)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
val position = viewHolder.absoluteAdapterPosition
|
||||||
|
if (position == PlayingQueue.currentIndex()) {
|
||||||
|
adapter.notifyItemChanged(position)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
PlayingQueue.remove(position)
|
||||||
|
adapter.notifyItemRemoved(position)
|
||||||
|
adapter.notifyItemRangeChanged(position, adapter.itemCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val itemTouchHelper = ItemTouchHelper(callback)
|
||||||
|
itemTouchHelper.attachToRecyclerView(binding.optionsRecycler)
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,10 @@ package com.github.libretube.ui.sheets
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.ShareObjectType
|
import com.github.libretube.constants.ShareObjectType
|
||||||
|
import com.github.libretube.extensions.toStreamItem
|
||||||
import com.github.libretube.ui.dialogs.AddToPlaylistDialog
|
import com.github.libretube.ui.dialogs.AddToPlaylistDialog
|
||||||
import com.github.libretube.ui.dialogs.DownloadDialog
|
import com.github.libretube.ui.dialogs.DownloadDialog
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog
|
import com.github.libretube.ui.dialogs.ShareDialog
|
||||||
@ -12,6 +14,9 @@ import com.github.libretube.ui.views.BottomSheet
|
|||||||
import com.github.libretube.util.BackgroundHelper
|
import com.github.libretube.util.BackgroundHelper
|
||||||
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 kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog with different options for a selected video.
|
* Dialog with different options for a selected video.
|
||||||
@ -79,10 +84,28 @@ class VideoOptionsBottomSheet(
|
|||||||
shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
|
shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
|
||||||
}
|
}
|
||||||
context?.getString(R.string.play_next) -> {
|
context?.getString(R.string.play_next) -> {
|
||||||
PlayingQueue.addAsNext(videoId)
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
PlayingQueue.addAsNext(
|
||||||
|
RetrofitInstance.api.getStreams(videoId)
|
||||||
|
.toStreamItem(videoId)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
context?.getString(R.string.add_to_queue) -> {
|
context?.getString(R.string.add_to_queue) -> {
|
||||||
PlayingQueue.add(videoId)
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
PlayingQueue.add(
|
||||||
|
RetrofitInstance.api.getStreams(videoId)
|
||||||
|
.toStreamItem(videoId)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.github.libretube.ui.viewholders
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.github.libretube.databinding.QueueRowBinding
|
||||||
|
|
||||||
|
class PlayingQueueViewHolder(
|
||||||
|
val binding: QueueRowBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root)
|
@ -1,92 +0,0 @@
|
|||||||
package com.github.libretube.util
|
|
||||||
|
|
||||||
import com.github.libretube.api.RetrofitInstance
|
|
||||||
import com.github.libretube.extensions.toID
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class AutoPlayHelper(
|
|
||||||
private val playlistId: String?
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val playlistStreamIds = mutableListOf<String>()
|
|
||||||
private var playlistNextPage: String? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the id of the next video to be played
|
|
||||||
*/
|
|
||||||
suspend fun getNextVideoId(
|
|
||||||
currentVideoId: String,
|
|
||||||
relatedStreams: List<com.github.libretube.api.obj.StreamItem>?
|
|
||||||
): String? {
|
|
||||||
return if (playlistId == null) {
|
|
||||||
getNextTrendingVideoId(
|
|
||||||
relatedStreams
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
getNextPlaylistVideoId(
|
|
||||||
currentVideoId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the id of the next related video
|
|
||||||
*/
|
|
||||||
private fun getNextTrendingVideoId(
|
|
||||||
relatedStreams: List<com.github.libretube.api.obj.StreamItem>?
|
|
||||||
): String? {
|
|
||||||
// don't play a video if it got played before already
|
|
||||||
if (relatedStreams == null || relatedStreams.isEmpty()) return null
|
|
||||||
var index = 0
|
|
||||||
var nextStreamId: String? = null
|
|
||||||
while (nextStreamId == null || PlayingQueue.containsBeforeCurrent(nextStreamId)) {
|
|
||||||
nextStreamId = relatedStreams[index].url!!.toID()
|
|
||||||
if (index + 1 < relatedStreams.size) {
|
|
||||||
index += 1
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nextStreamId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the videoId of the next video in a playlist
|
|
||||||
*/
|
|
||||||
private suspend fun getNextPlaylistVideoId(currentVideoId: String): String? {
|
|
||||||
// if the playlists contain the video, then save the next video as next stream
|
|
||||||
if (playlistStreamIds.contains(currentVideoId)) {
|
|
||||||
val index = playlistStreamIds.indexOf(currentVideoId)
|
|
||||||
// check whether there's a next video
|
|
||||||
return if (index + 1 < playlistStreamIds.size) {
|
|
||||||
playlistStreamIds[index + 1]
|
|
||||||
} else if (playlistNextPage == null) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
getNextPlaylistVideoId(currentVideoId)
|
|
||||||
}
|
|
||||||
} else if (playlistStreamIds.isEmpty() || playlistNextPage != null) {
|
|
||||||
// fetch the next page of the playlist
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
// fetch the playlists or its nextPage's videos
|
|
||||||
val playlist =
|
|
||||||
if (playlistNextPage == null) {
|
|
||||||
RetrofitInstance.authApi.getPlaylist(playlistId!!)
|
|
||||||
} else {
|
|
||||||
RetrofitInstance.authApi.getPlaylistNextPage(
|
|
||||||
playlistId!!,
|
|
||||||
playlistNextPage!!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// save the playlist urls to the list
|
|
||||||
playlistStreamIds += playlist.relatedStreams!!.map { it.url!!.toID() }
|
|
||||||
// save playlistNextPage for usage if video is not contained
|
|
||||||
playlistNextPage = playlist.nextpage
|
|
||||||
return@withContext getNextPlaylistVideoId(currentVideoId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// return null when no nextPage is found
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +1,114 @@
|
|||||||
package com.github.libretube.util
|
package com.github.libretube.util
|
||||||
|
|
||||||
object PlayingQueue {
|
import com.github.libretube.api.RetrofitInstance
|
||||||
private val queue = mutableListOf<String>()
|
import com.github.libretube.api.obj.StreamItem
|
||||||
private var currentVideoId: String? = null
|
import com.github.libretube.extensions.move
|
||||||
|
import com.github.libretube.extensions.toID
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
fun add(videoId: String) {
|
object PlayingQueue {
|
||||||
if (currentVideoId == videoId) return
|
private val queue = mutableListOf<StreamItem>()
|
||||||
if (queue.contains(videoId)) queue.remove(videoId)
|
private var currentStream: StreamItem? = null
|
||||||
queue.add(videoId)
|
|
||||||
|
fun add(vararg streamItem: StreamItem) {
|
||||||
|
streamItem.forEach {
|
||||||
|
if (currentStream != it) {
|
||||||
|
if (queue.contains(it)) queue.remove(it)
|
||||||
|
queue.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAsNext(videoId: String) {
|
fun addAsNext(streamItem: StreamItem) {
|
||||||
if (currentVideoId == videoId) return
|
if (currentStream == streamItem) return
|
||||||
if (queue.contains(videoId)) queue.remove(videoId)
|
if (queue.contains(streamItem)) queue.remove(streamItem)
|
||||||
queue.add(
|
queue.add(
|
||||||
queue.indexOf(currentVideoId) + 1,
|
currentIndex() + 1,
|
||||||
videoId
|
streamItem
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNext(): String? {
|
fun getNext(): String? {
|
||||||
return try {
|
return try {
|
||||||
queue[currentIndex() + 1]
|
queue[currentIndex() + 1].url?.toID()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPrev(): String? {
|
fun getPrev(): String? {
|
||||||
val index = queue.indexOf(currentVideoId)
|
val index = queue.indexOf(currentStream)
|
||||||
return if (index > 0) queue[index - 1] else null
|
return if (index > 0) queue[index - 1].url?.toID() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasPrev(): Boolean {
|
fun hasPrev(): Boolean {
|
||||||
return queue.indexOf(currentVideoId) > 0
|
return queue.indexOf(currentStream) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCurrent(videoId: String) {
|
fun updateCurrent(streamItem: StreamItem) {
|
||||||
currentVideoId = videoId
|
currentStream = streamItem
|
||||||
queue.add(videoId)
|
if (!contains(streamItem)) queue.add(streamItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isNotEmpty() = queue.isNotEmpty()
|
fun isNotEmpty() = queue.isNotEmpty()
|
||||||
|
|
||||||
|
fun isEmpty() = queue.isEmpty()
|
||||||
|
|
||||||
fun clear() = queue.clear()
|
fun clear() = queue.clear()
|
||||||
|
|
||||||
fun currentIndex() = queue.indexOf(currentVideoId)
|
fun size() = queue.size
|
||||||
|
|
||||||
fun contains(videoId: String) = queue.contains(videoId)
|
fun currentIndex(): Int {
|
||||||
|
return try {
|
||||||
|
queue.indexOf(
|
||||||
|
queue.first { it.url?.toID() == currentStream?.url?.toID() }
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun containsBeforeCurrent(videoId: String): Boolean {
|
fun contains(streamItem: StreamItem) = queue.any { it.url?.toID() == streamItem.url?.toID() }
|
||||||
return queue.contains(videoId) && queue.indexOf(videoId) < currentIndex()
|
|
||||||
|
fun getStreams() = queue
|
||||||
|
|
||||||
|
fun remove(index: Int) = queue.removeAt(index)
|
||||||
|
|
||||||
|
fun move(from: Int, to: Int) = queue.move(from, to)
|
||||||
|
|
||||||
|
private fun fetchMoreFromPlaylist(playlistId: String, nextPage: String?) {
|
||||||
|
var playlistNextPage: String? = nextPage
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
while (playlistNextPage != null) {
|
||||||
|
RetrofitInstance.authApi.getPlaylistNextPage(
|
||||||
|
playlistId,
|
||||||
|
playlistNextPage!!
|
||||||
|
).apply {
|
||||||
|
add(
|
||||||
|
*this.relatedStreams.orEmpty().toTypedArray()
|
||||||
|
)
|
||||||
|
playlistNextPage = this.nextpage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insertPlaylist(playlistId: String, newCurrentStream: StreamItem) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val response = RetrofitInstance.authApi.getPlaylist(playlistId)
|
||||||
|
add(
|
||||||
|
*response.relatedStreams
|
||||||
|
.orEmpty()
|
||||||
|
.toTypedArray()
|
||||||
|
)
|
||||||
|
updateCurrent(newCurrentStream)
|
||||||
|
fetchMoreFromPlaylist(playlistId, response.nextpage)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
app/src/main/res/drawable/ic_queue.xml
Normal file
10
app/src/main/res/drawable/ic_queue.xml
Normal 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="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M32,40q-2.45,0 -4.2,-1.675t-1.75,-4.075q0,-2.4 1.675,-4.075Q29.4,28.5 31.8,28.5q0.8,0 1.575,0.15 0.775,0.15 1.525,0.5L34.9,12L44,12v3.55h-6.1L37.9,34.3q0,2.35 -1.725,4.025Q34.45,40 32,40ZM6,31.5v-3h15.3v3ZM6,23.25v-3h23.65v3ZM6,15v-3h23.65v3Z" />
|
||||||
|
</vector>
|
@ -67,10 +67,17 @@
|
|||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layoutDirection="ltr">
|
android:layoutDirection="ltr">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/queue_toggle"
|
||||||
|
style="@style/PlayerControlTop"
|
||||||
|
android:layout_marginEnd="2dp"
|
||||||
|
android:src="@drawable/ic_queue"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:tint="@android:color/white" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/toggle_options"
|
android:id="@+id/toggle_options"
|
||||||
style="@style/PlayerControlTop"
|
style="@style/PlayerControlTop"
|
||||||
android:layout_marginTop="-1dp"
|
|
||||||
android:src="@drawable/ic_arrow_down"
|
android:src="@drawable/ic_arrow_down"
|
||||||
app:tint="@android:color/white" />
|
app:tint="@android:color/white" />
|
||||||
|
|
||||||
|
@ -298,6 +298,40 @@
|
|||||||
app:cornerRadius="11dp" />
|
app:cornerRadius="11dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/comments_toggle"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
app:cardCornerRadius="18dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/commentsToggle_textView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/comments"
|
||||||
|
android:textSize="17sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/commentsToggle_imageView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginEnd="3dp"
|
||||||
|
android:src="@drawable/ic_arrow_up_down" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -305,49 +339,10 @@
|
|||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
android:descendantFocusability="blocksDescendants">
|
android:descendantFocusability="blocksDescendants">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:id="@+id/comments_toggle"
|
|
||||||
style="@style/Widget.Material3.CardView.Elevated"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:cardCornerRadius="18dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/commentsToggle_textView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/comments"
|
|
||||||
android:textSize="17sp" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/commentsToggle_imageView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_marginEnd="3dp"
|
|
||||||
android:src="@drawable/ic_arrow_up_down" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/comments_recView"
|
android:id="@+id/comments_recView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/comments_toggle"
|
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
android:nestedScrollingEnabled="false"
|
android:nestedScrollingEnabled="false"
|
||||||
@ -357,7 +352,6 @@
|
|||||||
android:id="@+id/related_rec_view"
|
android:id="@+id/related_rec_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/comments_recView"
|
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="10dp"
|
||||||
|
62
app/src/main/res/layout/queue_row.xml
Normal file
62
app/src/main/res/layout/queue_row.xml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/rounded_ripple"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="10dp">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="45dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
app:cardCornerRadius="12dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/thumbnail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="I am Hardstyle - Episode 111" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/videoInfo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="12sp"
|
||||||
|
tools:text="Brennan Heart" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:src="@drawable/ic_drag" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -27,4 +27,10 @@
|
|||||||
android:title="@string/about"
|
android:title="@string/about"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_queue"
|
||||||
|
android:title="@string/queue"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:visible="false" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
@ -348,6 +348,8 @@
|
|||||||
<string name="show_more">Show more</string>
|
<string name="show_more">Show more</string>
|
||||||
<string name="time_code">Time code</string>
|
<string name="time_code">Time code</string>
|
||||||
<string name="added_to_playlist">Added to playlist</string>
|
<string name="added_to_playlist">Added to playlist</string>
|
||||||
|
<string name="playing_queue">Playing queue</string>
|
||||||
|
<string name="queue">Queue</string>
|
||||||
|
|
||||||
<!-- Notification channel strings -->
|
<!-- Notification channel strings -->
|
||||||
<string name="download_channel_name">Download Service</string>
|
<string name="download_channel_name">Download Service</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user