Merge branch 'libre-tube:master' into master

This commit is contained in:
Giles Munn 2022-10-23 20:14:50 +01:00 committed by GitHub
commit 9376a92fa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 510 additions and 246 deletions

2
.github/tg.py vendored
View File

@ -21,7 +21,7 @@ def bot():
Signed-off-by: {data['commit']['author']['name']} Signed-off-by: {data['commit']['author']['name']}
''', parse_mode=telegram.ParseMode.MARKDOWN) ''', parse_mode=telegram.ParseMode.MARKDOWN)
bot.send_media_group(TG_POST_ID, [telegram.InputMediaDocument(open('app-universal-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-x86-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-x86_64-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-armeabi-v7a-debug.apk', 'rb')), telegram.InputMediaDocument(open('app-arm64-v8a-debug.apk', 'rb'))]) bot.send_media_group(TG_POST_ID, [telegram.InputMediaDocument(open('app-universal-debug-signed.apk', 'rb')), telegram.InputMediaDocument(open('app-x86-debug-signed.apk', 'rb')), telegram.InputMediaDocument(open('app-x86_64-debug-signed.apk', 'rb')), telegram.InputMediaDocument(open('app-armeabi-v7a-debug-signed.apk', 'rb')), telegram.InputMediaDocument(open('app-arm64-v8a-debug-signed.apk', 'rb'))])
system('killall -9 python') system('killall -9 python')
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -54,6 +54,18 @@ jobs:
cd .. cd ..
./gradlew assembleDebug ./gradlew assembleDebug
- name: Sign Apk
continue-on-error: true
id: sign_apk
uses: ilharp/sign-android-release@v1
with:
releaseDir: app/build/outputs/apk/debug
signingKey: ${{ secrets.ANDROID_SIGNING_KEY }}
keyAlias: ${{ secrets.ANDROID_KEY_ALIAS }}
keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
- name: Upload to Archive - name: Upload to Archive
continue-on-error: true continue-on-error: true
run: | run: |
@ -61,7 +73,7 @@ jobs:
echo "GH_REPO = '${{ github.repository }}'" > tgconfig.py echo "GH_REPO = '${{ github.repository }}'" > tgconfig.py
git clone https://github.com/LibreTubeAlpha/Archive archive git clone https://github.com/LibreTubeAlpha/Archive archive
rm -rf archive/*.apk rm -rf archive/*.apk
mv app/build/outputs/apk/debug/*.apk archive/ mv app/build/outputs/apk/debug/*-signed.apk archive/
cd archive cd archive
python ../uploader.py python ../uploader.py
@ -86,4 +98,5 @@ jobs:
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: app name: app
path: archive/*.apk path: archive/*-signed.apk

View File

@ -41,6 +41,8 @@ android {
} }
debug { debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable true debuggable true
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
resValue "string", "app_name", "LibreTube Debug" resValue "string", "app_name", "LibreTube Debug"

View File

@ -16,15 +16,16 @@
# debugging stack traces. # debugging stack traces.
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
# prevents obfuscation in debug logs
-dontobfuscate
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
#uncomment for debug #uncomment for debug
#-keepnames class ** #-keepnames class **
# Keep data classes used for Retrofit
-keep class com.github.libretube.obj.** { *; } -keep class com.github.libretube.obj.** { *; }
-keep class com.github.libretube.obj.update.** { *; }
# prevents android from removing it
-keep class com.github.libretube.obj.**.** { *; }
# prevents obfuscation in debug logs
-dontobfuscate

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

@ -354,4 +354,6 @@
<string name="show_more">Daha çox göstər</string> <string name="show_more">Daha çox göstər</string>
<string name="time_code">Vaxt kodu</string> <string name="time_code">Vaxt kodu</string>
<string name="added_to_playlist">Pleylistə əlavə edildi</string> <string name="added_to_playlist">Pleylistə əlavə edildi</string>
<string name="playing_queue">Oynatma növbəsi</string>
<string name="queue">Növbə</string>
</resources> </resources>

View File

@ -224,7 +224,7 @@
<string name="matrix">ম্যাট্রিক্স</string> <string name="matrix">ম্যাট্রিক্স</string>
<string name="change_playback_speed">প্লে-ব্যাকের গতি</string> <string name="change_playback_speed">প্লে-ব্যাকের গতি</string>
<string name="selected">বাছাইকৃত</string> <string name="selected">বাছাইকৃত</string>
<string name="require_restart_message">এই চেঞ্জটি কার্যকর করতে হলে রিস্টার্ট হওয়া প্রয়োজন। অ্যাপ রিস্টার্ট করুন\?</string> <string name="require_restart_message">এই চেঞ্জটি কার্যকর করতে হলে রিস্টার্ট হওয়া প্রয়োজন। অ্যাপ রিস্টার্ট করতে \'আচ্ছা\'তে চাপুন।</string>
<string name="chapters">চ্যাপ্টার</string> <string name="chapters">চ্যাপ্টার</string>
<string name="require_restart">অ্যাপের রিস্টার্ট প্রয়োজন</string> <string name="require_restart">অ্যাপের রিস্টার্ট প্রয়োজন</string>
<string name="auth_instance">অথেন্টিকেশন ইন্সটেন্স</string> <string name="auth_instance">অথেন্টিকেশন ইন্সটেন্স</string>
@ -327,4 +327,31 @@
<string name="network_metered">Metered</string> <string name="network_metered">Metered</string>
<string name="new_videos_badge_summary">নতুন ভিডিওর সংখ্যাসহ ব্যাজ দেখান।</string> <string name="new_videos_badge_summary">নতুন ভিডিওর সংখ্যাসহ ব্যাজ দেখান।</string>
<string name="preferences">পছন্দসমূহ</string> <string name="preferences">পছন্দসমূহ</string>
<string name="save_feed">ফিড ব্যাকগ্রাউন্ডে লোড করুন</string>
<string name="save_feed_summary">ফিড ব্যাকগ্রাউন্ডে লোড করুন এবং অটো রিফ্রেশ হওয়া বন্ধ করুন।</string>
<string name="progressive_load_interval_summary">কম ভ্যালু ভিডিওর লোডিংয়ের সময় কমিয়ে আনে।</string>
<string name="play_next">পরেরটা চালান</string>
<string name="added_to_playlist">প্লে-লিস্টে অ্যাড করুন</string>
<string name="push_channel_name">নোটিফিকেশন ওয়ার্কার</string>
<string name="push_channel_description">নতুন স্ট্রিম আসলে নোটিফিকেশন দেখান।</string>
<string name="navigation_bar">নেভিগেশনবার</string>
<string name="select_at_least_one">অন্তত একটি আইটেম সিলেক্ট করুন</string>
<string name="backup_customInstances">কাস্টম ইন্সটেন্সগুলো</string>
<string name="default_load_interval">ডিফল্ট</string>
<string name="progressive_load_interval">Progressive load interval size</string>
<string name="playback_pitch">পিচ</string>
<string name="download_channel_name">ডাউনলোড সার্ভিস</string>
<string name="background_channel_description">অডিও প্লেয়ার নিয়ন্ত্রণের জন্য নোটিফিকেশনে বাটন দেখান।</string>
<string name="change_region">বর্তমান এলাকায় ট্রেডিং অ্যাভেলেবল নেই। সেটিংসে গিয়ে অন্যটি বাছুন।</string>
<string name="download_channel_description">ডাউনলোডের সময় নোটিফিকেশন দেখান।</string>
<string name="background_channel_name">ব্যাকগ্রাউন্ড মোড</string>
<string name="limit_hls">HLSকে 1080p তে সীমাবদ্ধ করুন</string>
<string name="filename">ফাইলের নাম</string>
<string name="invalid_filename">ইনভ্যালিড ফাইলনেম!</string>
<string name="playlists_order">প্লে-লিস্টের অর্ডার</string>
<string name="playlistNameReversed">নাম (উল্টো)</string>
<string name="recentlyUpdated">সম্প্রতি আপডেট করা হয়েছে</string>
<string name="recentlyUpdatedReversed">সম্প্রতি আপডেট করা হয়েছে (উল্টো)</string>
<string name="show_more">আরো দেখান</string>
<string name="time_code">সময় কোড</string>
</resources> </resources>

View File

@ -354,4 +354,6 @@
<string name="show_more">Mostrar más</string> <string name="show_more">Mostrar más</string>
<string name="time_code">código de tiempo</string> <string name="time_code">código de tiempo</string>
<string name="added_to_playlist">Se ha añadido a la lista de reproducción</string> <string name="added_to_playlist">Se ha añadido a la lista de reproducción</string>
<string name="queue">Cola</string>
<string name="playing_queue">Cola de la reproducción</string>
</resources> </resources>

View File

@ -351,4 +351,9 @@
<string name="change_region">På vei opp ser ut til å være utilgjengelig for nåværende region. Velg en annen i innstillingene.</string> <string name="change_region">På vei opp ser ut til å være utilgjengelig for nåværende region. Velg en annen i innstillingene.</string>
<string name="invalid_filename">Ugyldig filnavn</string> <string name="invalid_filename">Ugyldig filnavn</string>
<string name="playlists_order">Spillelisterekkefølge</string> <string name="playlists_order">Spillelisterekkefølge</string>
<string name="playing_queue">Spillekø</string>
<string name="queue"></string>
<string name="added_to_playlist">Lagt til på spillelisten</string>
<string name="show_more">Vis mer</string>
<string name="time_code">Tidskode</string>
</resources> </resources>

View File

@ -353,4 +353,5 @@
<string name="playlistNameReversed">Nazwa listy odtwarzania (odwrotnie)</string> <string name="playlistNameReversed">Nazwa listy odtwarzania (odwrotnie)</string>
<string name="limit_hls">Ogranicz HLS do 1080p</string> <string name="limit_hls">Ogranicz HLS do 1080p</string>
<string name="progressive_load_interval_summary">Niższa wartość może przyspieszyć początkowe ładowanie wideo.</string> <string name="progressive_load_interval_summary">Niższa wartość może przyspieszyć początkowe ładowanie wideo.</string>
<string name="added_to_playlist">Dodano do listy odtwarzania</string>
</resources> </resources>

View File

@ -49,7 +49,7 @@
<string name="vlcerror">VLC\'de açılamadı, kurulmamış olabilir.</string> <string name="vlcerror">VLC\'de açılamadı, kurulmamış olabilir.</string>
<string name="instances">Seç…</string> <string name="instances">Seç…</string>
<string name="customInstance">Özel</string> <string name="customInstance">Özel</string>
<string name="server_error">Sunucuyla ilgili bir sorun var. Başka bir örnek deneyelim mi\?</string> <string name="server_error">Sunucuyla ilgili bir sorun var. Başka bir örnek denensin mi\?</string>
<string name="grid">Ağ sütunları</string> <string name="grid">Ağ sütunları</string>
<string name="import_from_yt_summary">YouTube veya NewPipe\'tan</string> <string name="import_from_yt_summary">YouTube veya NewPipe\'tan</string>
<string name="emptyPlaylistName">Oynatma listesi adı boş olamaz</string> <string name="emptyPlaylistName">Oynatma listesi adı boş olamaz</string>
@ -148,7 +148,7 @@
<string name="sdcard">Hafıza kartı</string> <string name="sdcard">Hafıza kartı</string>
<string name="music_directory">Müzik klasörü</string> <string name="music_directory">Müzik klasörü</string>
<string name="defaultIcon">Varsayılan</string> <string name="defaultIcon">Varsayılan</string>
<string name="gradientIcon">Pergar eğilimi</string> <string name="gradientIcon">Akıcı değişim</string>
<string name="fireIcon">Moda ateş</string> <string name="fireIcon">Moda ateş</string>
<string name="torchIcon">Şık fener</string> <string name="torchIcon">Şık fener</string>
<string name="birdIcon">Hızlandırılmış kuş</string> <string name="birdIcon">Hızlandırılmış kuş</string>
@ -233,9 +233,9 @@
<string name="reset_watch_positions">Sıfırla</string> <string name="reset_watch_positions">Sıfırla</string>
<string name="data_saver_mode_summary">Küçük resimleri ve diğer resimleri gösterme.</string> <string name="data_saver_mode_summary">Küçük resimleri ve diğer resimleri gösterme.</string>
<string name="update_now">Yeni LibreTube sürümü şimdi yüklensin mi\?</string> <string name="update_now">Yeni LibreTube sürümü şimdi yüklensin mi\?</string>
<string name="navLabelVisibility">Navigasyon çubuğundaki etiket görünürlüğü</string> <string name="navLabelVisibility">Gezinme çubuğundaki etiket görünürlüğü</string>
<string name="always">Her zaman</string> <string name="always">Her zaman</string>
<string name="selected">Seçildi</string> <string name="selected">Seçilen</string>
<string name="never">Hiçbir zaman</string> <string name="never">Hiçbir zaman</string>
<string name="autoRotatePlayer">Otomatik tam ekran</string> <string name="autoRotatePlayer">Otomatik tam ekran</string>
<string name="autoRotatePlayer_summary">Cihaz döndüğünde tam ekran oynatma.</string> <string name="autoRotatePlayer_summary">Cihaz döndüğünde tam ekran oynatma.</string>
@ -354,4 +354,6 @@
<string name="time_code">Zaman kodu</string> <string name="time_code">Zaman kodu</string>
<string name="show_more">Daha fazla göster</string> <string name="show_more">Daha fazla göster</string>
<string name="added_to_playlist">Oynatma listesine eklendi</string> <string name="added_to_playlist">Oynatma listesine eklendi</string>
<string name="queue">Sıra</string>
<string name="playing_queue">Oynatma sırası</string>
</resources> </resources>

View File

@ -354,4 +354,6 @@
<string name="show_more">Показати більше</string> <string name="show_more">Показати більше</string>
<string name="time_code">Часовий код</string> <string name="time_code">Часовий код</string>
<string name="added_to_playlist">Додано в добірку</string> <string name="added_to_playlist">Додано в добірку</string>
<string name="playing_queue">Черга відтворення</string>
<string name="queue">Черга</string>
</resources> </resources>

View File

@ -354,4 +354,6 @@
<string name="show_more">显示更多</string> <string name="show_more">显示更多</string>
<string name="time_code">时间码</string> <string name="time_code">时间码</string>
<string name="added_to_playlist">已添加到播放列表</string> <string name="added_to_playlist">已添加到播放列表</string>
<string name="playing_queue">播放队列</string>
<string name="queue">队列</string>
</resources> </resources>

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>