diff --git a/app/src/main/java/com/github/libretube/extensions/Move.kt b/app/src/main/java/com/github/libretube/extensions/Move.kt new file mode 100644 index 000000000..bd556be87 --- /dev/null +++ b/app/src/main/java/com/github/libretube/extensions/Move.kt @@ -0,0 +1,7 @@ +package com.github.libretube.extensions + +fun MutableList.move(oldPosition: Int, newPosition: Int) { + val item = this.get(oldPosition) + this.removeAt(oldPosition) + this.add(newPosition, item) +} diff --git a/app/src/main/java/com/github/libretube/extensions/ToStreamItem.kt b/app/src/main/java/com/github/libretube/extensions/ToStreamItem.kt new file mode 100644 index 000000000..fee0f85a9 --- /dev/null +++ b/app/src/main/java/com/github/libretube/extensions/ToStreamItem.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt index 9cda3e3cc..4134ab485 100644 --- a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt +++ b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt @@ -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) } diff --git a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt index 32286d1dd..b366b2b63 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt @@ -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) } } diff --git a/app/src/main/java/com/github/libretube/ui/adapters/PlayingQueueAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/PlayingQueueAdapter.kt new file mode 100644 index 000000000..af9236e46 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/adapters/PlayingQueueAdapter.kt @@ -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() { + 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) + ) + } + } + } +} diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 5606b1bbb..c7ea66253 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -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 ) { diff --git a/app/src/main/java/com/github/libretube/ui/sheets/PlayingQueueSheet.kt b/app/src/main/java/com/github/libretube/ui/sheets/PlayingQueueSheet.kt new file mode 100644 index 000000000..1292b7377 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/sheets/PlayingQueueSheet.kt @@ -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) + } +} diff --git a/app/src/main/java/com/github/libretube/ui/sheets/VideoOptionsBottomSheet.kt b/app/src/main/java/com/github/libretube/ui/sheets/VideoOptionsBottomSheet.kt index f174e2a33..5310b75a1 100644 --- a/app/src/main/java/com/github/libretube/ui/sheets/VideoOptionsBottomSheet.kt +++ b/app/src/main/java/com/github/libretube/ui/sheets/VideoOptionsBottomSheet.kt @@ -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() + } + } } } } diff --git a/app/src/main/java/com/github/libretube/ui/viewholders/PlayingQueueViewHolder.kt b/app/src/main/java/com/github/libretube/ui/viewholders/PlayingQueueViewHolder.kt new file mode 100644 index 000000000..9fe6f200d --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/viewholders/PlayingQueueViewHolder.kt @@ -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) diff --git a/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt b/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt deleted file mode 100644 index 1e1f17e1f..000000000 --- a/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt +++ /dev/null @@ -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() - private var playlistNextPage: String? = null - - /** - * get the id of the next video to be played - */ - suspend fun getNextVideoId( - currentVideoId: String, - relatedStreams: List? - ): String? { - return if (playlistId == null) { - getNextTrendingVideoId( - relatedStreams - ) - } else { - getNextPlaylistVideoId( - currentVideoId - ) - } - } - - /** - * get the id of the next related video - */ - private fun getNextTrendingVideoId( - relatedStreams: List? - ): 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 - } -} diff --git a/app/src/main/java/com/github/libretube/util/PlayingQueue.kt b/app/src/main/java/com/github/libretube/util/PlayingQueue.kt index 7063c1d7d..887185ae8 100644 --- a/app/src/main/java/com/github/libretube/util/PlayingQueue.kt +++ b/app/src/main/java/com/github/libretube/util/PlayingQueue.kt @@ -1,55 +1,114 @@ package com.github.libretube.util -object PlayingQueue { - private val queue = mutableListOf() - 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() + 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() + } + } } } diff --git a/app/src/main/res/drawable/ic_queue.xml b/app/src/main/res/drawable/ic_queue.xml new file mode 100644 index 000000000..1bc9c0aad --- /dev/null +++ b/app/src/main/res/drawable/ic_queue.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/exo_styled_player_control_view.xml b/app/src/main/res/layout/exo_styled_player_control_view.xml index 6aaeb0d73..e508662f6 100644 --- a/app/src/main/res/layout/exo_styled_player_control_view.xml +++ b/app/src/main/res/layout/exo_styled_player_control_view.xml @@ -67,10 +67,17 @@ android:layout_gravity="center" android:layoutDirection="ltr"> + + diff --git a/app/src/main/res/layout/fragment_player.xml b/app/src/main/res/layout/fragment_player.xml index 01cfb7bdd..f6326dda2 100644 --- a/app/src/main/res/layout/fragment_player.xml +++ b/app/src/main/res/layout/fragment_player.xml @@ -298,6 +298,40 @@ app:cornerRadius="11dp" /> + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/action_bar.xml b/app/src/main/res/menu/action_bar.xml index 960c25e58..90488f1d8 100644 --- a/app/src/main/res/menu/action_bar.xml +++ b/app/src/main/res/menu/action_bar.xml @@ -27,4 +27,10 @@ android:title="@string/about" app:showAsAction="never" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ece153900..e69f7711e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -348,6 +348,8 @@ Show more Time code Added to playlist + Playing queue + Queue Download Service