mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 06:10:31 +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.DatabaseHolder
|
||||
import com.github.libretube.extensions.awaitQuery
|
||||
import com.github.libretube.extensions.toID
|
||||
import com.github.libretube.util.AutoPlayHelper
|
||||
import com.github.libretube.extensions.toStreamItem
|
||||
import com.github.libretube.util.NowPlayingNotification
|
||||
import com.github.libretube.util.PlayerHelper
|
||||
import com.github.libretube.util.PlayingQueue
|
||||
@ -80,16 +79,6 @@ class BackgroundMode : Service() {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -132,9 +121,6 @@ class BackgroundMode : Service() {
|
||||
playlistId = intent.getStringExtra(IntentData.playlistId)
|
||||
val position = intent.getLongExtra(IntentData.position, 0L)
|
||||
|
||||
// initialize the playlist autoPlay Helper
|
||||
autoPlayHelper = AutoPlayHelper(playlistId)
|
||||
|
||||
// play the audio in the background
|
||||
loadAudio(videoId, position)
|
||||
|
||||
@ -146,7 +132,9 @@ class BackgroundMode : Service() {
|
||||
}
|
||||
|
||||
private fun updateWatchPosition() {
|
||||
player?.currentPosition?.let { DatabaseHelper.saveWatchPosition(videoId, it) }
|
||||
player?.currentPosition?.let {
|
||||
DatabaseHelper.saveWatchPosition(videoId, it)
|
||||
}
|
||||
handler.postDelayed(this::updateWatchPosition, 500)
|
||||
}
|
||||
|
||||
@ -157,8 +145,6 @@ class BackgroundMode : Service() {
|
||||
videoId: String,
|
||||
seekToPosition: Long = 0
|
||||
) {
|
||||
// append the video to the playing queue
|
||||
PlayingQueue.add(videoId)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
streams = RetrofitInstance.api.getStreams(videoId)
|
||||
@ -166,6 +152,17 @@ class BackgroundMode : Service() {
|
||||
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 {
|
||||
playAudio(seekToPosition)
|
||||
}
|
||||
@ -175,8 +172,6 @@ class BackgroundMode : Service() {
|
||||
private fun playAudio(
|
||||
seekToPosition: Long
|
||||
) {
|
||||
PlayingQueue.updateCurrent(videoId)
|
||||
|
||||
initializePlayer()
|
||||
setMediaItem()
|
||||
|
||||
@ -218,8 +213,6 @@ class BackgroundMode : Service() {
|
||||
player?.setPlaybackSpeed(playbackSpeed)
|
||||
|
||||
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)
|
||||
*/
|
||||
private fun playNextVideo() {
|
||||
if (nextStreamId == null || nextStreamId == videoId) return
|
||||
val nextQueueVideo = PlayingQueue.getNext()
|
||||
if (nextQueueVideo != null) nextStreamId = nextQueueVideo
|
||||
val nextVideo = PlayingQueue.getNext()
|
||||
|
||||
// play new video on background
|
||||
this.videoId = nextStreamId!!
|
||||
if (nextVideo != null) {
|
||||
this.videoId = nextVideo
|
||||
}
|
||||
this.segmentData = null
|
||||
loadAudio(videoId)
|
||||
}
|
||||
|
@ -34,8 +34,10 @@ import com.github.libretube.services.ClosingService
|
||||
import com.github.libretube.ui.base.BaseActivity
|
||||
import com.github.libretube.ui.dialogs.ErrorDialog
|
||||
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.NetworkHelper
|
||||
import com.github.libretube.util.PlayingQueue
|
||||
import com.github.libretube.util.PreferenceHelper
|
||||
import com.github.libretube.util.ThemeHelper
|
||||
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
|
||||
*/
|
||||
@ -302,6 +309,10 @@ class MainActivity : BaseActivity() {
|
||||
startActivity(communityIntent)
|
||||
true
|
||||
}
|
||||
R.id.action_queue -> {
|
||||
PlayingQueueSheet().show(supportFragmentManager, null)
|
||||
true
|
||||
}
|
||||
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.query
|
||||
import com.github.libretube.extensions.toID
|
||||
import com.github.libretube.extensions.toStreamItem
|
||||
import com.github.libretube.models.PlayerViewModel
|
||||
import com.github.libretube.models.interfaces.PlayerOptionsInterface
|
||||
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.DownloadDialog
|
||||
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.util.AutoPlayHelper
|
||||
import com.github.libretube.util.BackgroundHelper
|
||||
import com.github.libretube.util.ImageHelper
|
||||
import com.github.libretube.util.NowPlayingNotification
|
||||
@ -153,12 +154,6 @@ class PlayerFragment : BaseFragment() {
|
||||
private var token = PreferenceHelper.getToken()
|
||||
private var videoShownInExternalPlayer = false
|
||||
|
||||
/**
|
||||
* for autoplay
|
||||
*/
|
||||
private var nextStreamId: String? = null
|
||||
private lateinit var autoPlayHelper: AutoPlayHelper
|
||||
|
||||
/**
|
||||
* for the player notification
|
||||
*/
|
||||
@ -409,6 +404,11 @@ class PlayerFragment : BaseFragment() {
|
||||
toggleComments()
|
||||
}
|
||||
|
||||
playerBinding.queueToggle.visibility = View.VISIBLE
|
||||
playerBinding.queueToggle.setOnClickListener {
|
||||
PlayingQueueSheet().show(childFragmentManager, null)
|
||||
}
|
||||
|
||||
// FullScreen button trigger
|
||||
// hide fullscreen button if auto rotation enabled
|
||||
playerBinding.fullscreen.visibility =
|
||||
@ -630,8 +630,6 @@ class PlayerFragment : BaseFragment() {
|
||||
|
||||
private fun playVideo() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
PlayingQueue.updateCurrent(videoId!!)
|
||||
|
||||
streams = try {
|
||||
RetrofitInstance.api.getStreams(videoId!!)
|
||||
} catch (e: IOException) {
|
||||
@ -645,6 +643,21 @@ class PlayerFragment : BaseFragment() {
|
||||
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 {
|
||||
// hide the button to skip SponsorBlock segments manually
|
||||
binding.sbSkipBtn.visibility = View.GONE
|
||||
@ -668,8 +681,6 @@ class PlayerFragment : BaseFragment() {
|
||||
if (PlayerHelper.sponsorBlockEnabled) fetchSponsorBlockSegments()
|
||||
// show comments if related streams disabled
|
||||
if (!PlayerHelper.relatedStreamsEnabled) toggleComments()
|
||||
// prepare for autoplay
|
||||
if (binding.player.autoplayEnabled) setNextStream()
|
||||
|
||||
// add the video to the watch history
|
||||
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
|
||||
*/
|
||||
@ -758,18 +758,19 @@ class PlayerFragment : BaseFragment() {
|
||||
|
||||
// used for autoplay and skipping to next video
|
||||
private fun playNextVideo() {
|
||||
if (nextStreamId == null) return
|
||||
// check whether there is a new video in the queue
|
||||
val nextQueueVideo = PlayingQueue.getNext()
|
||||
if (nextQueueVideo != null) nextStreamId = nextQueueVideo
|
||||
val nextVideoId = PlayingQueue.getNext()
|
||||
// by making sure that the next and the current video aren't the same
|
||||
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
|
||||
videoId = nextStreamId
|
||||
playVideo()
|
||||
if (nextVideoId != null) {
|
||||
videoId = nextVideoId
|
||||
|
||||
// forces the comments to reload for the new video
|
||||
commentsLoaded = false
|
||||
binding.commentsRecView.adapter = null
|
||||
playVideo()
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareExoPlayerView() {
|
||||
@ -866,7 +867,6 @@ class PlayerFragment : BaseFragment() {
|
||||
@Suppress("DEPRECATION")
|
||||
if (
|
||||
playbackState == Player.STATE_ENDED &&
|
||||
nextStreamId != null &&
|
||||
!transitioning &&
|
||||
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.widget.Toast
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.constants.IntentData
|
||||
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.DownloadDialog
|
||||
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.PlayingQueue
|
||||
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.
|
||||
@ -79,10 +84,28 @@ class VideoOptionsBottomSheet(
|
||||
shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
|
||||
}
|
||||
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) -> {
|
||||
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
|
||||
|
||||
object PlayingQueue {
|
||||
private val queue = mutableListOf<String>()
|
||||
private var currentVideoId: String? = null
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
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) {
|
||||
if (currentVideoId == videoId) return
|
||||
if (queue.contains(videoId)) queue.remove(videoId)
|
||||
queue.add(videoId)
|
||||
object PlayingQueue {
|
||||
private val queue = mutableListOf<StreamItem>()
|
||||
private var currentStream: StreamItem? = null
|
||||
|
||||
fun add(vararg streamItem: StreamItem) {
|
||||
streamItem.forEach {
|
||||
if (currentStream != it) {
|
||||
if (queue.contains(it)) queue.remove(it)
|
||||
queue.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAsNext(videoId: String) {
|
||||
if (currentVideoId == videoId) return
|
||||
if (queue.contains(videoId)) queue.remove(videoId)
|
||||
fun addAsNext(streamItem: StreamItem) {
|
||||
if (currentStream == streamItem) return
|
||||
if (queue.contains(streamItem)) queue.remove(streamItem)
|
||||
queue.add(
|
||||
queue.indexOf(currentVideoId) + 1,
|
||||
videoId
|
||||
currentIndex() + 1,
|
||||
streamItem
|
||||
)
|
||||
}
|
||||
|
||||
fun getNext(): String? {
|
||||
return try {
|
||||
queue[currentIndex() + 1]
|
||||
queue[currentIndex() + 1].url?.toID()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getPrev(): String? {
|
||||
val index = queue.indexOf(currentVideoId)
|
||||
return if (index > 0) queue[index - 1] else null
|
||||
val index = queue.indexOf(currentStream)
|
||||
return if (index > 0) queue[index - 1].url?.toID() else null
|
||||
}
|
||||
|
||||
fun hasPrev(): Boolean {
|
||||
return queue.indexOf(currentVideoId) > 0
|
||||
return queue.indexOf(currentStream) > 0
|
||||
}
|
||||
|
||||
fun updateCurrent(videoId: String) {
|
||||
currentVideoId = videoId
|
||||
queue.add(videoId)
|
||||
fun updateCurrent(streamItem: StreamItem) {
|
||||
currentStream = streamItem
|
||||
if (!contains(streamItem)) queue.add(streamItem)
|
||||
}
|
||||
|
||||
fun isNotEmpty() = queue.isNotEmpty()
|
||||
|
||||
fun isEmpty() = queue.isEmpty()
|
||||
|
||||
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 {
|
||||
return queue.contains(videoId) && queue.indexOf(videoId) < currentIndex()
|
||||
fun contains(streamItem: StreamItem) = queue.any { it.url?.toID() == streamItem.url?.toID() }
|
||||
|
||||
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: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
|
||||
android:id="@+id/toggle_options"
|
||||
style="@style/PlayerControlTop"
|
||||
android:layout_marginTop="-1dp"
|
||||
android:src="@drawable/ic_arrow_down"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
|
@ -298,6 +298,40 @@
|
||||
app:cornerRadius="11dp" />
|
||||
</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
|
||||
android:layout_width="match_parent"
|
||||
@ -305,49 +339,10 @@
|
||||
android:animateLayoutChanges="true"
|
||||
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
|
||||
android:id="@+id/comments_recView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/comments_toggle"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
@ -357,7 +352,6 @@
|
||||
android:id="@+id/related_rec_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/comments_recView"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="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"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_queue"
|
||||
android:title="@string/queue"
|
||||
app:showAsAction="never"
|
||||
android:visible="false" />
|
||||
|
||||
</menu>
|
@ -348,6 +348,8 @@
|
||||
<string name="show_more">Show more</string>
|
||||
<string name="time_code">Time code</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 -->
|
||||
<string name="download_channel_name">Download Service</string>
|
||||
|
Loading…
Reference in New Issue
Block a user