mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 14:20:30 +05:30
Merge pull request #2678 from Bnyro/audio-player
Integrated audio player UI
This commit is contained in:
commit
0632ce05d0
@ -8,7 +8,7 @@ object IntentData {
|
|||||||
const val timeStamp = "timeStamp"
|
const val timeStamp = "timeStamp"
|
||||||
const val position = "position"
|
const val position = "position"
|
||||||
const val fileName = "fileName"
|
const val fileName = "fileName"
|
||||||
const val openQueueOnce = "openQueue"
|
|
||||||
const val keepQueue = "keepQueue"
|
const val keepQueue = "keepQueue"
|
||||||
const val playlistType = "playlistType"
|
const val playlistType = "playlistType"
|
||||||
|
const val openAudioPlayer = "openAudioPlayer"
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,6 @@ object PreferenceKeys {
|
|||||||
* Background mode
|
* Background mode
|
||||||
*/
|
*/
|
||||||
const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
|
const val BACKGROUND_PLAYBACK_SPEED = "background_playback_speed"
|
||||||
const val NOTIFICATION_OPEN_QUEUE = "notification_open_queue"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications
|
* Notifications
|
||||||
|
@ -5,6 +5,7 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Binder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
@ -87,6 +88,16 @@ class BackgroundMode : Service() {
|
|||||||
*/
|
*/
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for connecting to the AudioPlayerFragment
|
||||||
|
*/
|
||||||
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for passing playback state changes to the AudioPlayerFragment
|
||||||
|
*/
|
||||||
|
var onIsPlayingChanged: ((isPlaying: Boolean) -> Unit)? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setting the required [Notification] for running as a foreground service
|
* Setting the required [Notification] for running as a foreground service
|
||||||
*/
|
*/
|
||||||
@ -251,6 +262,11 @@ class BackgroundMode : Service() {
|
|||||||
* Plays the next video when the current one ended
|
* Plays the next video when the current one ended
|
||||||
*/
|
*/
|
||||||
player?.addListener(object : Player.Listener {
|
player?.addListener(object : Player.Listener {
|
||||||
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
super.onIsPlayingChanged(isPlaying)
|
||||||
|
onIsPlayingChanged?.invoke(isPlaying)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(state: Int) {
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
when (state) {
|
when (state) {
|
||||||
Player.STATE_ENDED -> {
|
Player.STATE_ENDED -> {
|
||||||
@ -381,7 +397,26 @@ class BackgroundMode : Service() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(p0: Intent?): IBinder? {
|
inner class LocalBinder : Binder() {
|
||||||
return null
|
// Return this instance of [BackgroundMode] so clients can call public methods
|
||||||
|
fun getService(): BackgroundMode = this@BackgroundMode
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(p0: Intent?): IBinder {
|
||||||
|
return binder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentPosition() = player?.currentPosition
|
||||||
|
|
||||||
|
fun getDuration() = player?.duration
|
||||||
|
|
||||||
|
fun seekToPosition(position: Long) = player?.seekTo(position)
|
||||||
|
|
||||||
|
fun pause() {
|
||||||
|
player?.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun play() {
|
||||||
|
player?.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,11 +39,9 @@ import com.github.libretube.ui.fragments.PlayerFragment
|
|||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.PlayerViewModel
|
||||||
import com.github.libretube.ui.models.SearchViewModel
|
import com.github.libretube.ui.models.SearchViewModel
|
||||||
import com.github.libretube.ui.models.SubscriptionsViewModel
|
import com.github.libretube.ui.models.SubscriptionsViewModel
|
||||||
import com.github.libretube.ui.sheets.PlayingQueueSheet
|
|
||||||
import com.github.libretube.ui.tools.BreakReminder
|
import com.github.libretube.ui.tools.BreakReminder
|
||||||
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.elevation.SurfaceColors
|
import com.google.android.material.elevation.SurfaceColors
|
||||||
@ -221,11 +219,6 @@ 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
|
||||||
*/
|
*/
|
||||||
@ -373,10 +366,6 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -391,6 +380,11 @@ class MainActivity : BaseActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent?.getBooleanExtra(IntentData.openAudioPlayer, false) == true) {
|
||||||
|
navController.navigate(R.id.audioPlayerFragment)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
intent?.getStringExtra(IntentData.channelId)?.let {
|
intent?.getStringExtra(IntentData.channelId)?.let {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
R.id.channelFragment,
|
R.id.channelFragment,
|
||||||
@ -412,6 +406,7 @@ class MainActivity : BaseActivity() {
|
|||||||
intent?.getStringExtra(IntentData.videoId)?.let {
|
intent?.getStringExtra(IntentData.videoId)?.let {
|
||||||
loadVideo(it, intent?.getLongExtra(IntentData.timeStamp, 0L))
|
loadVideo(it, intent?.getLongExtra(IntentData.timeStamp, 0L))
|
||||||
}
|
}
|
||||||
|
|
||||||
when (intent?.getStringExtra("fragmentToOpen")) {
|
when (intent?.getStringExtra("fragmentToOpen")) {
|
||||||
"home" ->
|
"home" ->
|
||||||
navController.navigate(R.id.homeFragment)
|
navController.navigate(R.id.homeFragment)
|
||||||
@ -422,10 +417,6 @@ class MainActivity : BaseActivity() {
|
|||||||
"library" ->
|
"library" ->
|
||||||
navController.navigate(R.id.libraryFragment)
|
navController.navigate(R.id.libraryFragment)
|
||||||
}
|
}
|
||||||
if (intent?.getBooleanExtra(IntentData.openQueueOnce, false) == true) {
|
|
||||||
PlayingQueueSheet()
|
|
||||||
.show(supportFragmentManager)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadVideo(videoId: String, timeStamp: Long?) {
|
private fun loadVideo(videoId: String, timeStamp: Long?) {
|
||||||
|
@ -0,0 +1,181 @@
|
|||||||
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.github.libretube.R
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
import com.github.libretube.databinding.FragmentAudioPlayerBinding
|
||||||
|
import com.github.libretube.extensions.toID
|
||||||
|
import com.github.libretube.services.BackgroundMode
|
||||||
|
import com.github.libretube.ui.activities.MainActivity
|
||||||
|
import com.github.libretube.ui.base.BaseFragment
|
||||||
|
import com.github.libretube.ui.sheets.PlayingQueueSheet
|
||||||
|
import com.github.libretube.util.ImageHelper
|
||||||
|
import com.github.libretube.util.NavigationHelper
|
||||||
|
import com.github.libretube.util.PlayingQueue
|
||||||
|
|
||||||
|
class AudioPlayerFragment : BaseFragment() {
|
||||||
|
private lateinit var binding: FragmentAudioPlayerBinding
|
||||||
|
private val onTrackChangeListener: (StreamItem) -> Unit = {
|
||||||
|
updateStreamInfo()
|
||||||
|
}
|
||||||
|
private var handler = Handler(Looper.getMainLooper())
|
||||||
|
private var isPaused: Boolean = false
|
||||||
|
|
||||||
|
private lateinit var playerService: BackgroundMode
|
||||||
|
|
||||||
|
/** Defines callbacks for service binding, passed to bindService() */
|
||||||
|
private val connection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||||
|
// We've bound to LocalService, cast the IBinder and get LocalService instance
|
||||||
|
val binder = service as BackgroundMode.LocalBinder
|
||||||
|
playerService = binder.getService()
|
||||||
|
handleServiceConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||||
|
val mainActivity = activity as MainActivity
|
||||||
|
if (mainActivity.navController.currentDestination?.id == R.id.audioPlayerFragment) {
|
||||||
|
mainActivity.navController.popBackStack()
|
||||||
|
} else {
|
||||||
|
mainActivity.navController.backQueue.removeAll {
|
||||||
|
it.destination.id == R.id.audioPlayerFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
Intent(activity, BackgroundMode::class.java).also { intent ->
|
||||||
|
activity?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentAudioPlayerBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.prev.setOnClickListener {
|
||||||
|
val currentIndex = PlayingQueue.currentIndex()
|
||||||
|
if (!PlayingQueue.hasPrev()) return@setOnClickListener
|
||||||
|
PlayingQueue.onQueueItemSelected(currentIndex - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.next.setOnClickListener {
|
||||||
|
val currentIndex = PlayingQueue.currentIndex()
|
||||||
|
if (!PlayingQueue.hasNext()) return@setOnClickListener
|
||||||
|
PlayingQueue.onQueueItemSelected(currentIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.thumbnail.setOnClickListener {
|
||||||
|
PlayingQueueSheet().show(childFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for track changes due to autoplay or the notification
|
||||||
|
PlayingQueue.addOnTrackChangedListener(onTrackChangeListener)
|
||||||
|
|
||||||
|
binding.playPause.setOnClickListener {
|
||||||
|
if (!this::playerService.isInitialized) return@setOnClickListener
|
||||||
|
if (isPaused) playerService.play() else playerService.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the stream info into the UI
|
||||||
|
updateStreamInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the information from a new stream into the UI
|
||||||
|
*/
|
||||||
|
private fun updateStreamInfo() {
|
||||||
|
val current = PlayingQueue.getCurrent()
|
||||||
|
current ?: return
|
||||||
|
|
||||||
|
binding.title.text = current.title
|
||||||
|
binding.uploader.text = current.uploaderName
|
||||||
|
binding.uploader.setOnClickListener {
|
||||||
|
NavigationHelper.navigateChannel(requireContext(), current.uploaderUrl?.toID())
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageHelper.loadImage(current.thumbnail, binding.thumbnail)
|
||||||
|
|
||||||
|
initializeSeekBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeSeekBar() {
|
||||||
|
if (!this::playerService.isInitialized) return
|
||||||
|
|
||||||
|
binding.timeBar.addOnChangeListener { _, value, fromUser ->
|
||||||
|
if (fromUser) playerService.seekToPosition(value.toLong() * 1000)
|
||||||
|
}
|
||||||
|
updateSeekBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the position, duration and text views belonging to the seek bar
|
||||||
|
*/
|
||||||
|
private fun updateSeekBar() {
|
||||||
|
val duration = playerService.getDuration()?.toFloat() ?: return
|
||||||
|
|
||||||
|
// when the video is not loaded yet, retry in 100 ms
|
||||||
|
if (duration <= 0) {
|
||||||
|
handler.postDelayed(this::updateSeekBar, 100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the current position from the player service
|
||||||
|
val currentPosition = playerService.getCurrentPosition()?.toFloat() ?: 0f
|
||||||
|
|
||||||
|
// set the text for the indicators
|
||||||
|
binding.duration.text = DateUtils.formatElapsedTime((duration / 1000).toLong())
|
||||||
|
binding.currentPosition.text = DateUtils.formatElapsedTime(
|
||||||
|
(currentPosition / 1000).toLong()
|
||||||
|
)
|
||||||
|
|
||||||
|
// update the time bar current value and maximum value
|
||||||
|
binding.timeBar.valueTo = duration / 1000
|
||||||
|
binding.timeBar.value = minOf(
|
||||||
|
currentPosition / 1000,
|
||||||
|
binding.timeBar.valueTo
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.postDelayed(this::updateSeekBar, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleServiceConnection() {
|
||||||
|
playerService.onIsPlayingChanged = { isPlaying ->
|
||||||
|
binding.playPause.setIconResource(
|
||||||
|
if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play
|
||||||
|
)
|
||||||
|
isPaused = !isPlaying
|
||||||
|
}
|
||||||
|
initializeSeekBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
// unregister all listeners and the connected [playerService]
|
||||||
|
playerService.onIsPlayingChanged = null
|
||||||
|
activity?.unbindService(connection)
|
||||||
|
PlayingQueue.removeOnTrackChangedListener(onTrackChangeListener)
|
||||||
|
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
@ -350,13 +350,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
BackgroundHelper.stopBackgroundPlay(requireContext())
|
BackgroundHelper.stopBackgroundPlay(requireContext())
|
||||||
}
|
}
|
||||||
playerBinding.closeImageButton.setOnClickListener {
|
playerBinding.closeImageButton.setOnClickListener {
|
||||||
viewModel.isFullscreen.value = false
|
killFragment()
|
||||||
binding.playerMotionLayout.transitionToEnd()
|
|
||||||
val mainActivity = activity as MainActivity
|
|
||||||
mainActivity.supportFragmentManager.beginTransaction()
|
|
||||||
.remove(this)
|
|
||||||
.commit()
|
|
||||||
BackgroundHelper.stopBackgroundPlay(requireContext())
|
|
||||||
}
|
}
|
||||||
playerBinding.autoPlay.visibility = View.VISIBLE
|
playerBinding.autoPlay.visibility = View.VISIBLE
|
||||||
|
|
||||||
@ -473,6 +467,7 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun playOnBackground() {
|
private fun playOnBackground() {
|
||||||
|
BackgroundHelper.stopBackgroundPlay(requireContext())
|
||||||
BackgroundHelper.playOnBackground(
|
BackgroundHelper.playOnBackground(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
videoId!!,
|
videoId!!,
|
||||||
@ -480,6 +475,10 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
playlistId,
|
playlistId,
|
||||||
channelId
|
channelId
|
||||||
)
|
)
|
||||||
|
handler.postDelayed({
|
||||||
|
(activity as MainActivity).navController.navigate(R.id.audioPlayerFragment)
|
||||||
|
killFragment()
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFullscreen() {
|
private fun setFullscreen() {
|
||||||
@ -1520,6 +1519,15 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
|
|||||||
return exoPlayer.isPlaying && !backgroundModeRunning
|
return exoPlayer.isPlaying && !backgroundModeRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun killFragment() {
|
||||||
|
viewModel.isFullscreen.value = false
|
||||||
|
binding.playerMotionLayout.transitionToEnd()
|
||||||
|
val mainActivity = activity as MainActivity
|
||||||
|
mainActivity.supportFragmentManager.beginTransaction()
|
||||||
|
.remove(this)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ import com.github.libretube.api.obj.Streams
|
|||||||
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
|
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
|
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
|
||||||
import com.github.libretube.ui.activities.MainActivity
|
import com.github.libretube.ui.activities.MainActivity
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
@ -75,11 +74,7 @@ class NowPlayingNotification(
|
|||||||
// that's the only way to launch back into the previous activity (e.g. the player view
|
// that's the only way to launch back into the previous activity (e.g. the player view
|
||||||
val intent = Intent(context, MainActivity::class.java).apply {
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
if (isBackgroundPlayerNotification) {
|
if (isBackgroundPlayerNotification) {
|
||||||
if (PreferenceHelper.getBoolean(PreferenceKeys.NOTIFICATION_OPEN_QUEUE, true)) {
|
putExtra(IntentData.openAudioPlayer, true)
|
||||||
putExtra(IntentData.openQueueOnce, true)
|
|
||||||
} else {
|
|
||||||
putExtra(IntentData.videoId, videoId)
|
|
||||||
}
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,16 @@ import kotlinx.coroutines.launch
|
|||||||
object PlayingQueue {
|
object PlayingQueue {
|
||||||
private val queue = mutableListOf<StreamItem>()
|
private val queue = mutableListOf<StreamItem>()
|
||||||
private var currentStream: StreamItem? = null
|
private var currentStream: StreamItem? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener that gets called when the user selects an item from the queue
|
||||||
|
*/
|
||||||
private var onQueueTapListener: (StreamItem) -> Unit = {}
|
private var onQueueTapListener: (StreamItem) -> Unit = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener that gets called when the current playing video changes
|
||||||
|
*/
|
||||||
|
private val onTrackChangedListeners: MutableList<(StreamItem) -> Unit> = mutableListOf()
|
||||||
var repeatQueue: Boolean = false
|
var repeatQueue: Boolean = false
|
||||||
|
|
||||||
fun add(vararg streamItem: StreamItem) {
|
fun add(vararg streamItem: StreamItem) {
|
||||||
@ -58,6 +67,11 @@ object PlayingQueue {
|
|||||||
|
|
||||||
fun updateCurrent(streamItem: StreamItem) {
|
fun updateCurrent(streamItem: StreamItem) {
|
||||||
currentStream = streamItem
|
currentStream = streamItem
|
||||||
|
onTrackChangedListeners.forEach {
|
||||||
|
runCatching {
|
||||||
|
it.invoke(streamItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!contains(streamItem)) queue.add(streamItem)
|
if (!contains(streamItem)) queue.add(streamItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +91,8 @@ object PlayingQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCurrent(): StreamItem? = currentStream
|
||||||
|
|
||||||
fun contains(streamItem: StreamItem) = queue.any { it.url?.toID() == streamItem.url?.toID() }
|
fun contains(streamItem: StreamItem) = queue.any { it.url?.toID() == streamItem.url?.toID() }
|
||||||
|
|
||||||
// only returns a copy of the queue, no write access
|
// only returns a copy of the queue, no write access
|
||||||
@ -162,9 +178,18 @@ object PlayingQueue {
|
|||||||
onQueueTapListener = listener
|
onQueueTapListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addOnTrackChangedListener(listener: (StreamItem) -> Unit) {
|
||||||
|
onTrackChangedListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeOnTrackChangedListener(listener: (StreamItem) -> Unit) {
|
||||||
|
onTrackChangedListeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
fun resetToDefaults() {
|
fun resetToDefaults() {
|
||||||
repeatQueue = false
|
repeatQueue = false
|
||||||
onQueueTapListener = {}
|
onQueueTapListener = {}
|
||||||
|
onTrackChangedListeners.clear()
|
||||||
queue.clear()
|
queue.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
121
app/src/main/res/layout/fragment_audio_player.xml
Normal file
121
app/src/main/res/layout/fragment_audio_player.xml
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<?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="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/thumbnail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="20dp"
|
||||||
|
android:layout_marginVertical="10dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Small"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="20dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/uploader"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:layout_marginTop="10dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/time_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:labelBehavior="gone"
|
||||||
|
android:layout_marginHorizontal="20dp" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="20dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/current_position"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start|center"
|
||||||
|
tools:text="00:00"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/duration"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center"
|
||||||
|
tools:text="10:15"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginVertical="50dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/prev"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="20dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_prev" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/play_pause"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:insetLeft="0dp"
|
||||||
|
android:insetTop="0dp"
|
||||||
|
android:insetRight="0dp"
|
||||||
|
android:insetBottom="0dp"
|
||||||
|
app:icon="@drawable/ic_pause"
|
||||||
|
app:iconSize="24dp"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Full" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/next"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="20dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_next" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -25,10 +25,4 @@
|
|||||||
android:title="@string/about"
|
android:title="@string/about"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_queue"
|
|
||||||
android:title="@string/queue"
|
|
||||||
android:visible="false"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
</menu>
|
</menu>
|
@ -54,4 +54,9 @@
|
|||||||
android:name="com.github.libretube.ui.fragments.DownloadsFragment"
|
android:name="com.github.libretube.ui.fragments.DownloadsFragment"
|
||||||
android:label="@string/downloads"
|
android:label="@string/downloads"
|
||||||
tools:layout="@layout/fragment_downloads" />
|
tools:layout="@layout/fragment_downloads" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/audioPlayerFragment"
|
||||||
|
android:name="com.github.libretube.ui.fragments.AudioPlayerFragment"
|
||||||
|
android:label="@string/audio_player"
|
||||||
|
tools:layout="@layout/fragment_audio_player" />
|
||||||
</navigation>
|
</navigation>
|
@ -430,6 +430,7 @@
|
|||||||
<string name="pause">Pause</string>
|
<string name="pause">Pause</string>
|
||||||
<string name="alternative_pip_controls">Alternative PiP controls</string>
|
<string name="alternative_pip_controls">Alternative PiP controls</string>
|
||||||
<string name="alternative_pip_controls_summary">Show audio only and skip controls in PiP instead of forward and rewind</string>
|
<string name="alternative_pip_controls_summary">Show audio only and skip controls in PiP instead of forward and rewind</string>
|
||||||
|
<string name="audio_player">Audio player</string>
|
||||||
|
|
||||||
<!-- Notification channel strings -->
|
<!-- Notification channel strings -->
|
||||||
<string name="download_channel_name">Download Service</string>
|
<string name="download_channel_name">Download Service</string>
|
||||||
|
@ -89,12 +89,6 @@
|
|||||||
app:valueFrom="0.2"
|
app:valueFrom="0.2"
|
||||||
app:valueTo="4.0" />
|
app:valueTo="4.0" />
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:defaultValue="true"
|
|
||||||
android:icon="@drawable/ic_grid"
|
|
||||||
android:title="@string/open_queue_from_notification"
|
|
||||||
app:key="notification_open_queue" />
|
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user