Merge pull request #1647 from Bnyro/queue

New playing queue
This commit is contained in:
Bnyro 2022-10-23 16:06:25 +02:00 committed by GitHub
commit 6ab3c96f87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 437 additions and 232 deletions

View File

@ -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)
}

View File

@ -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
)
}

View File

@ -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)
} }

View File

@ -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)
} }
} }

View File

@ -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)
)
}
}
}
}

View File

@ -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
) { ) {

View File

@ -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)
}
}

View File

@ -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()
}
}
} }
} }
} }

View File

@ -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)

View File

@ -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
}
}

View File

@ -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()
}
}
} }
} }

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="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>

View File

@ -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" />

View File

@ -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"

View 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>

View File

@ -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>

View File

@ -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>