add episodes list to player [Part 2]
This commit is contained in:
parent
6fc7bb2c1e
commit
04893060e4
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -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 }
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user