add episodes list to player [Part 2]

This commit is contained in:
Jannik 2020-12-15 23:15:14 +01:00
parent 6fc7bb2c1e
commit 04893060e4
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
9 changed files with 102 additions and 91 deletions

View File

@ -11,7 +11,7 @@ android {
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 30
versionCode 2100 //00.02.100 versionCode 2100 //00.02.100
versionName "0.2.90" versionName "0.2.91"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime() resValue "string", "build_time", buildTime()

View File

@ -29,9 +29,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.components.EpisodesPlayer import org.mosad.teapod.ui.components.EpisodesListPlayer
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.Episode
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.scheduleAtFixedRate import kotlin.concurrent.scheduleAtFixedRate
@ -125,6 +124,12 @@ class PlayerActivity : AppCompatActivity() {
initExoPlayer() initExoPlayer()
initVideoView() initVideoView()
initTimeUpdates() initTimeUpdates()
// add listener after initial media is started
model.currentEpisodeChangedListener.add {
nextEpManually = true // make sure on STATE_ENDED doesn't skip another episode
playCurrentMedia(false)
}
} }
private fun initExoPlayer() { private fun initExoPlayer() {
@ -153,16 +158,15 @@ class PlayerActivity : AppCompatActivity() {
if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) { if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) {
// if next episode btn was clicked, skipp playNextEpisode() on STATE_ENDED // if next episode btn was clicked, skipp playNextEpisode() on STATE_ENDED
if (nextEpManually) { if (!nextEpManually) {
nextEpManually = false
} else {
playNextEpisode() playNextEpisode()
} }
nextEpManually = false
} }
} }
}) })
playCurrentMedia(true) playCurrentMedia(true) // start initial media
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -295,19 +299,12 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun togglePausePlay() { private fun togglePausePlay() {
if (player.isPlaying) { if (player.isPlaying) player.pause() else player.play()
player.pause()
} else {
player.play()
}
} }
private fun playNextEpisode() = model.nextEpisode?.let { private fun playNextEpisode() = model.nextEpisode?.let {
model.nextEpisode() // current = next, next = new or null model.nextEpisode() // current = next, next = new or null
hideButtonNextEp() hideButtonNextEp()
nextEpManually = true
playCurrentMedia(false)
} }
/** /**
@ -330,30 +327,16 @@ class PlayerActivity : AppCompatActivity() {
button_next_ep_c.visibility = View.GONE button_next_ep_c.visibility = View.GONE
} }
// update player/media item
player.playWhenReady = true
player.clearMediaItems() //remove previous item player.clearMediaItems() //remove previous item
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
MediaItem.fromUri(Uri.parse(autoSelectStream(model.currentEpisode))) MediaItem.fromUri(Uri.parse(model.autoSelectStream(model.currentEpisode)))
) )
if (seekToPosition) player.seekTo(playbackPosition) if (seekToPosition) player.seekTo(playbackPosition)
player.setMediaSource(mediaSource) player.setMediaSource(mediaSource)
player.prepare() player.prepare()
}
/**
* If preferSecondary or priStreamUrl is empty and secondary is present (secStreamOmU),
* use the secondary stream. Else, if the primary stream is set use the primary stream.
* If no stream is present, close the activity.
*/
private fun autoSelectStream(episode: Episode): String {
return if ((Preferences.preferSecondary || episode.priStreamUrl.isEmpty()) && episode.secStreamOmU) {
episode.secStreamUrl
} else if (episode.priStreamUrl.isNotEmpty()) {
episode.priStreamUrl
} else {
Log.e(javaClass.name, "No stream url set.")
this.finish()
""
}
} }
/** /**
@ -390,8 +373,6 @@ class PlayerActivity : AppCompatActivity() {
.setListener(null) .setListener(null)
} }
/** /**
* hide the next episode button * hide the next episode button
* TODO improve the hide animation * TODO improve the hide animation
@ -409,8 +390,14 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun showEpisodesList() { private fun showEpisodesList() {
val rootView = window.decorView.rootView as ViewGroup val episodesList = EpisodesListPlayer(this, model = model).apply {
EpisodesPlayer(rootView, model) onViewRemovedAction = { player.play() }
}
player_layout.addView(episodesList)
// hide player controls and pause playback
player.pause()
controller.hideImmediately()
} }
inner class PlayerGestureListener : GestureDetector.SimpleOnGestureListener() { inner class PlayerGestureListener : GestureDetector.SimpleOnGestureListener() {
@ -431,11 +418,7 @@ class PlayerActivity : AppCompatActivity() {
val viewCenterX = video_view.measuredWidth / 2 val viewCenterX = video_view.measuredWidth / 2
// if the event position is on the left side rewind, if it's on the right forward // if the event position is on the left side rewind, if it's on the right forward
if (eventPosX < viewCenterX) { if (eventPosX < viewCenterX) rewind() else fastForward()
rewind()
} else {
fastForward()
}
return true return true
} }

View File

@ -1,15 +1,25 @@
package org.mosad.teapod.player package org.mosad.teapod.player
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.fragments.MediaFragment import org.mosad.teapod.ui.fragments.MediaFragment
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.Episode import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.Media import org.mosad.teapod.util.Media
import kotlin.properties.Delegates
/**
* PlayerViewModel handles all stuff related to media/episodes.
* When currentEpisode is changed the player will start playing it (not initial media),
* the next episode will be update and the callback is handled.
*/
class PlayerViewModel : ViewModel() { class PlayerViewModel : ViewModel() {
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
var mediaId = 0 var mediaId = 0
internal set internal set
var episodeId = 0 var episodeId = 0
@ -17,8 +27,11 @@ class PlayerViewModel : ViewModel() {
var media: Media = Media(0, "", DataTypes.MediaType.OTHER) var media: Media = Media(0, "", DataTypes.MediaType.OTHER)
internal set internal set
var currentEpisode = Episode() var currentEpisode: Episode by Delegates.observable(Episode()) { _, _, _ ->
internal set currentEpisodeChangedListener.forEach { it() }
MediaFragment.instance.updateWatchedState(currentEpisode) // watchedCallback for the new episode
nextEpisode = selectNextEpisode() // update next ep
}
var nextEpisode: Episode? = null var nextEpisode: Episode? = null
internal set internal set
@ -32,6 +45,24 @@ class PlayerViewModel : ViewModel() {
currentEpisode = media.episodes.first { it.id == episodeId } currentEpisode = media.episodes.first { it.id == episodeId }
nextEpisode = selectNextEpisode() nextEpisode = selectNextEpisode()
currentEpisode
}
/**
* If preferSecondary or priStreamUrl is empty and secondary is present (secStreamOmU),
* use the secondary stream. Else, if the primary stream is set use the primary stream.
* If no stream is present, return empty string.
*/
fun autoSelectStream(episode: Episode): String {
return if ((Preferences.preferSecondary || episode.priStreamUrl.isEmpty()) && episode.secStreamOmU) {
episode.secStreamUrl
} else if (episode.priStreamUrl.isNotEmpty()) {
episode.priStreamUrl
} else {
Log.e(javaClass.name, "No stream url set. ${episode.id}")
""
}
} }
/** /**
@ -41,9 +72,6 @@ class PlayerViewModel : ViewModel() {
fun nextEpisode() = nextEpisode?.let { nextEp -> fun nextEpisode() = nextEpisode?.let { nextEp ->
currentEpisode = nextEp // set current ep to next ep currentEpisode = nextEp // set current ep to next ep
episodeId = nextEp.id episodeId = nextEp.id
MediaFragment.instance.updateWatchedState(nextEp) // watchedCallback for next ep
nextEpisode = selectNextEpisode()
} }
/** /**
@ -52,7 +80,7 @@ class PlayerViewModel : ViewModel() {
*/ */
private fun selectNextEpisode(): Episode? { private fun selectNextEpisode(): Episode? {
val nextEpIndex = media.episodes.indexOfFirst { it.id == currentEpisode.id } + 1 val nextEpIndex = media.episodes.indexOfFirst { it.id == currentEpisode.id } + 1
return if (nextEpIndex < (media.episodes.size)) { return if (nextEpIndex < media.episodes.size) {
media.episodes[nextEpIndex] media.episodes[nextEpIndex]
} else { } else {
null null

View File

@ -0,0 +1,43 @@
package org.mosad.teapod.ui.components
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import org.mosad.teapod.databinding.PlayerEpisodesListBinding
import org.mosad.teapod.player.PlayerViewModel
import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter
class EpisodesListPlayer @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
model: PlayerViewModel? = null
) : LinearLayout(context, attrs, defStyleAttr) {
private val binding = PlayerEpisodesListBinding.inflate(LayoutInflater.from(context), this, true)
private lateinit var adapterRecEpisodes: PlayerEpisodeItemAdapter
var onViewRemovedAction: (() -> Unit)? = null // TODO find a better solution for this
init {
binding.buttonCloseEpisodesList.setOnClickListener {
(this.parent as ViewGroup).removeView(this)
onViewRemovedAction?.invoke()
}
model?.let {
adapterRecEpisodes = PlayerEpisodeItemAdapter(it.media.episodes)
adapterRecEpisodes.onImageClick = { _, position ->
(this.parent as ViewGroup).removeView(this)
it.currentEpisode = it.media.episodes[position]
}
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
binding.recyclerEpisodesPlayer.scrollToPosition(it.currentEpisode.number - 1) // number != index
}
}
}

View File

@ -1,37 +0,0 @@
package org.mosad.teapod.ui.components
import android.view.LayoutInflater
import android.view.ViewGroup
import org.mosad.teapod.databinding.PlayerEpisodesBinding
import org.mosad.teapod.player.PlayerViewModel
import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter
/**
* TODO toggle play/pause on close/open
* TODO play selected episode
* TODO scroll to current episode
*/
class EpisodesPlayer(val parent: ViewGroup, private val model: PlayerViewModel) {
private val binding = PlayerEpisodesBinding.inflate(LayoutInflater.from(parent.context), parent, true)
private var adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.episodes)
init {
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
initActions()
}
private fun initActions() {
binding.buttonCloseEpisodesList.setOnClickListener {
parent.removeView(binding.root)
}
adapterRecEpisodes.onImageClick = { _, position ->
println(model.media.episodes[position])
//playEpisode(media.episodes[position])
}
}
}

View File

@ -155,12 +155,6 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
private fun playEpisode(ep: Episode) { private fun playEpisode(ep: Episode) {
playStream(ep) playStream(ep)
// update watched state
updateWatchedState(ep)
//AoDParser.sendCallback(ep.watchedCallback)
//adapterRecEpisodes.updateWatchedState(true, media.episodes.indexOf(ep))
//adapterRecEpisodes.notifyDataSetChanged()
// update nextEpisode // update nextEpisode
nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
media.episodes.first{ !it.watched } media.episodes.first{ !it.watched }

View File

@ -61,6 +61,7 @@ data class Info(
/** /**
* if secStreamOmU == true, then a secondary stream is present * if secStreamOmU == true, then a secondary stream is present
* number = episode number (0..n)
*/ */
data class Episode( data class Episode(
val id: Int = 0, val id: Int = 0,

View File

@ -138,7 +138,6 @@
android:layout_marginEnd="7dp" android:layout_marginEnd="7dp"
android:text="@string/episodes" android:text="@string/episodes"
android:textAllCaps="false" android:textAllCaps="false"
android:visibility="gone"
app:icon="@drawable/ic_baseline_video_library_24" app:icon="@drawable/ic_baseline_video_library_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button_next_ep_c" app:layout_constraintEnd_toStartOf="@+id/button_next_ep_c"