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
targetSdkVersion 30
versionCode 2100 //00.02.100
versionName "0.2.90"
versionName "0.2.91"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()

View File

@ -29,9 +29,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.mosad.teapod.R
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.Episode
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.scheduleAtFixedRate
@ -125,6 +124,12 @@ class PlayerActivity : AppCompatActivity() {
initExoPlayer()
initVideoView()
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() {
@ -153,16 +158,15 @@ class PlayerActivity : AppCompatActivity() {
if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) {
// if next episode btn was clicked, skipp playNextEpisode() on STATE_ENDED
if (nextEpManually) {
nextEpManually = false
} else {
if (!nextEpManually) {
playNextEpisode()
}
nextEpManually = false
}
}
})
playCurrentMedia(true)
playCurrentMedia(true) // start initial media
}
@SuppressLint("ClickableViewAccessibility")
@ -295,19 +299,12 @@ class PlayerActivity : AppCompatActivity() {
}
private fun togglePausePlay() {
if (player.isPlaying) {
player.pause()
} else {
player.play()
}
if (player.isPlaying) player.pause() else player.play()
}
private fun playNextEpisode() = model.nextEpisode?.let {
model.nextEpisode() // current = next, next = new or null
hideButtonNextEp()
nextEpManually = true
playCurrentMedia(false)
}
/**
@ -330,30 +327,16 @@ class PlayerActivity : AppCompatActivity() {
button_next_ep_c.visibility = View.GONE
}
// update player/media item
player.playWhenReady = true
player.clearMediaItems() //remove previous item
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)
player.setMediaSource(mediaSource)
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)
}
/**
* hide the next episode button
* TODO improve the hide animation
@ -409,8 +390,14 @@ class PlayerActivity : AppCompatActivity() {
}
private fun showEpisodesList() {
val rootView = window.decorView.rootView as ViewGroup
EpisodesPlayer(rootView, model)
val episodesList = EpisodesListPlayer(this, model = model).apply {
onViewRemovedAction = { player.play() }
}
player_layout.addView(episodesList)
// hide player controls and pause playback
player.pause()
controller.hideImmediately()
}
inner class PlayerGestureListener : GestureDetector.SimpleOnGestureListener() {
@ -431,11 +418,7 @@ class PlayerActivity : AppCompatActivity() {
val viewCenterX = video_view.measuredWidth / 2
// if the event position is on the left side rewind, if it's on the right forward
if (eventPosX < viewCenterX) {
rewind()
} else {
fastForward()
}
if (eventPosX < viewCenterX) rewind() else fastForward()
return true
}

View File

@ -1,15 +1,25 @@
package org.mosad.teapod.player
import android.util.Log
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.runBlocking
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.fragments.MediaFragment
import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.Episode
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() {
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
var mediaId = 0
internal set
var episodeId = 0
@ -17,8 +27,11 @@ class PlayerViewModel : ViewModel() {
var media: Media = Media(0, "", DataTypes.MediaType.OTHER)
internal set
var currentEpisode = Episode()
internal set
var currentEpisode: Episode by Delegates.observable(Episode()) { _, _, _ ->
currentEpisodeChangedListener.forEach { it() }
MediaFragment.instance.updateWatchedState(currentEpisode) // watchedCallback for the new episode
nextEpisode = selectNextEpisode() // update next ep
}
var nextEpisode: Episode? = null
internal set
@ -32,6 +45,24 @@ class PlayerViewModel : ViewModel() {
currentEpisode = media.episodes.first { it.id == episodeId }
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 ->
currentEpisode = nextEp // set current ep to next ep
episodeId = nextEp.id
MediaFragment.instance.updateWatchedState(nextEp) // watchedCallback for next ep
nextEpisode = selectNextEpisode()
}
/**
@ -52,7 +80,7 @@ class PlayerViewModel : ViewModel() {
*/
private fun selectNextEpisode(): Episode? {
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]
} else {
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) {
playStream(ep)
// update watched state
updateWatchedState(ep)
//AoDParser.sendCallback(ep.watchedCallback)
//adapterRecEpisodes.updateWatchedState(true, media.episodes.indexOf(ep))
//adapterRecEpisodes.notifyDataSetChanged()
// update nextEpisode
nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
media.episodes.first{ !it.watched }

View File

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

View File

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