Merge pull request 'Add Episodes List to Player' (#18) from feature/episodes_list_player into master
Reviewed-on: #18
This commit is contained in:
		@ -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,8 +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.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
 | 
				
			||||||
@ -124,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() {
 | 
				
			||||||
@ -151,16 +157,16 @@ class PlayerActivity : AppCompatActivity() {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) {
 | 
					                if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) {
 | 
				
			||||||
                    if (nextEpManually) {
 | 
					                    // if next episode btn was clicked, skipp playNextEpisode() on STATE_ENDED
 | 
				
			||||||
                        nextEpManually = false
 | 
					                    if (!nextEpManually) {
 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        playNextEpisode()
 | 
					                        playNextEpisode()
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    nextEpManually = false
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        playCurrentMedia(true)
 | 
					        playCurrentMedia(true) // start initial media
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @SuppressLint("ClickableViewAccessibility")
 | 
					    @SuppressLint("ClickableViewAccessibility")
 | 
				
			||||||
@ -187,6 +193,7 @@ class PlayerActivity : AppCompatActivity() {
 | 
				
			|||||||
        ffwd_10.setOnButtonClickListener { fastForward() }
 | 
					        ffwd_10.setOnButtonClickListener { fastForward() }
 | 
				
			||||||
        button_next_ep.setOnClickListener { playNextEpisode() }
 | 
					        button_next_ep.setOnClickListener { playNextEpisode() }
 | 
				
			||||||
        button_next_ep_c.setOnClickListener { playNextEpisode() }
 | 
					        button_next_ep_c.setOnClickListener { playNextEpisode() }
 | 
				
			||||||
 | 
					        button_episodes.setOnClickListener { showEpisodesList() }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun initTimeUpdates() {
 | 
					    private fun initTimeUpdates() {
 | 
				
			||||||
@ -292,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)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -314,7 +314,11 @@ class PlayerActivity : AppCompatActivity() {
 | 
				
			|||||||
    private fun playCurrentMedia(seekToPosition: Boolean) {
 | 
					    private fun playCurrentMedia(seekToPosition: Boolean) {
 | 
				
			||||||
        // update the gui
 | 
					        // update the gui
 | 
				
			||||||
        exo_text_title.text = if (model.media.type == DataTypes.MediaType.TVSHOW) {
 | 
					        exo_text_title.text = if (model.media.type == DataTypes.MediaType.TVSHOW) {
 | 
				
			||||||
            getString(R.string.component_episode_title, model.currentEpisode.number, model.currentEpisode.description)
 | 
					            getString(
 | 
				
			||||||
 | 
					                R.string.component_episode_title,
 | 
				
			||||||
 | 
					                model.currentEpisode.number,
 | 
				
			||||||
 | 
					                model.currentEpisode.description
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            model.currentEpisode.title
 | 
					            model.currentEpisode.title
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -323,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()
 | 
					 | 
				
			||||||
            ""
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -383,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
 | 
				
			||||||
@ -401,6 +389,17 @@ class PlayerActivity : AppCompatActivity() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun showEpisodesList() {
 | 
				
			||||||
 | 
					        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() {
 | 
					    inner class PlayerGestureListener : GestureDetector.SimpleOnGestureListener() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
@ -419,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
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					package org.mosad.teapod.util.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.view.LayoutInflater
 | 
				
			||||||
 | 
					import android.view.ViewGroup
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.RecyclerView
 | 
				
			||||||
 | 
					import com.bumptech.glide.Glide
 | 
				
			||||||
 | 
					import com.bumptech.glide.request.RequestOptions
 | 
				
			||||||
 | 
					import jp.wasabeef.glide.transformations.RoundedCornersTransformation
 | 
				
			||||||
 | 
					import org.mosad.teapod.R
 | 
				
			||||||
 | 
					import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
 | 
				
			||||||
 | 
					import org.mosad.teapod.util.Episode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PlayerEpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var onImageClick: ((String, Int) -> Unit)? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
 | 
				
			||||||
 | 
					        return EpisodeViewHolder(ItemEpisodePlayerBinding.inflate(LayoutInflater.from(parent.context), parent, false))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
 | 
				
			||||||
 | 
					        val context = holder.binding.root.context
 | 
				
			||||||
 | 
					        val ep = episodes[position]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val titleText = if (ep.priStreamUrl.isEmpty() && ep.secStreamOmU) {
 | 
				
			||||||
 | 
					            context.getString(R.string.component_episode_title_sub, ep.number, ep.description)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            context.getString(R.string.component_episode_title, ep.number, ep.description)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        holder.binding.textEpisodeTitle2.text = titleText
 | 
				
			||||||
 | 
					        holder.binding.textEpisodeDesc2.text = ep.shortDesc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (episodes[position].posterUrl.isNotEmpty()) {
 | 
				
			||||||
 | 
					            Glide.with(context).load(ep.posterUrl)
 | 
				
			||||||
 | 
					                .apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
 | 
				
			||||||
 | 
					                .into(holder.binding.imageEpisode)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getItemCount(): Int {
 | 
				
			||||||
 | 
					        return episodes.size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inner class EpisodeViewHolder(val binding: ItemEpisodePlayerBinding) : RecyclerView.ViewHolder(binding.root) {
 | 
				
			||||||
 | 
					        init {
 | 
				
			||||||
 | 
					            binding.imageEpisode.setOnClickListener {
 | 
				
			||||||
 | 
					                onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="utf-8"?>
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
 | 
					<androidx.core.widget.NestedScrollView 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"
 | 
					    xmlns:tools="http://schemas.android.com/tools"
 | 
				
			||||||
    android:layout_width="match_parent"
 | 
					    android:layout_width="match_parent"
 | 
				
			||||||
    android:layout_height="match_parent"
 | 
					    android:layout_height="match_parent"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										50
									
								
								app/src/main/res/layout/item_episode_player.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/src/main/res/layout/item_episode_player.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					<?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="wrap_content"
 | 
				
			||||||
 | 
					    android:layout_height="match_parent"
 | 
				
			||||||
 | 
					    android:orientation="vertical"
 | 
				
			||||||
 | 
					    android:padding="7dp" >
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <FrameLayout
 | 
				
			||||||
 | 
					        android:layout_width="wrap_content"
 | 
				
			||||||
 | 
					        android:layout_height="wrap_content">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <com.google.android.material.imageview.ShapeableImageView
 | 
				
			||||||
 | 
					            android:id="@+id/image_episode"
 | 
				
			||||||
 | 
					            android:layout_width="192dp"
 | 
				
			||||||
 | 
					            android:layout_height="108dp"
 | 
				
			||||||
 | 
					            android:contentDescription="@string/component_poster_desc"
 | 
				
			||||||
 | 
					            app:shapeAppearance="@style/ShapeAppearance.Teapod.RoundedPoster"
 | 
				
			||||||
 | 
					            app:srcCompat="@color/md_disabled_text_dark_theme" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ImageView
 | 
				
			||||||
 | 
					            android:id="@+id/image_episode_play"
 | 
				
			||||||
 | 
					            android:layout_width="wrap_content"
 | 
				
			||||||
 | 
					            android:layout_height="wrap_content"
 | 
				
			||||||
 | 
					            android:layout_gravity="center"
 | 
				
			||||||
 | 
					            android:background="@drawable/bg_circle__black_transparent_24dp"
 | 
				
			||||||
 | 
					            android:contentDescription="@string/button_play"
 | 
				
			||||||
 | 
					            app:srcCompat="@drawable/ic_baseline_play_arrow_24"
 | 
				
			||||||
 | 
					            app:tint="#FFFFFF" />
 | 
				
			||||||
 | 
					    </FrameLayout>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <TextView
 | 
				
			||||||
 | 
					        android:id="@+id/text_episode_title2"
 | 
				
			||||||
 | 
					        android:layout_width="match_parent"
 | 
				
			||||||
 | 
					        android:layout_height="wrap_content"
 | 
				
			||||||
 | 
					        android:layout_marginTop="7dp"
 | 
				
			||||||
 | 
					        android:text="@string/component_episode_title"
 | 
				
			||||||
 | 
					        android:textColor="@color/textPrimaryDark"
 | 
				
			||||||
 | 
					        android:textSize="16sp" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <TextView
 | 
				
			||||||
 | 
					        android:id="@+id/text_episode_desc2"
 | 
				
			||||||
 | 
					        android:layout_width="match_parent"
 | 
				
			||||||
 | 
					        android:layout_height="match_parent"
 | 
				
			||||||
 | 
					        android:layout_marginTop="5dp"
 | 
				
			||||||
 | 
					        android:text="@string/text_overview_ex"
 | 
				
			||||||
 | 
					        android:textColor="@color/textPrimaryDark"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</LinearLayout>
 | 
				
			||||||
@ -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"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										43
									
								
								app/src/main/res/layout/player_episodes_list.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/src/main/res/layout/player_episodes_list.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<androidx.constraintlayout.widget.ConstraintLayout 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:background="#73000000">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <LinearLayout
 | 
				
			||||||
 | 
					        android:id="@+id/linearLayout3"
 | 
				
			||||||
 | 
					        android:layout_width="0dp"
 | 
				
			||||||
 | 
					        android:layout_height="wrap_content"
 | 
				
			||||||
 | 
					        android:layout_marginStart="12dp"
 | 
				
			||||||
 | 
					        android:layout_marginTop="7dp"
 | 
				
			||||||
 | 
					        android:layout_marginEnd="12dp"
 | 
				
			||||||
 | 
					        android:orientation="horizontal"
 | 
				
			||||||
 | 
					        app:layout_constraintEnd_toEndOf="parent"
 | 
				
			||||||
 | 
					        app:layout_constraintStart_toStartOf="parent"
 | 
				
			||||||
 | 
					        app:layout_constraintTop_toTopOf="parent">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ImageButton
 | 
				
			||||||
 | 
					            android:id="@+id/button_close_episodes_list"
 | 
				
			||||||
 | 
					            style="@style/ExoStyledControls.Button.Center"
 | 
				
			||||||
 | 
					            android:layout_width="44dp"
 | 
				
			||||||
 | 
					            android:layout_height="44dp"
 | 
				
			||||||
 | 
					            android:contentDescription="@string/close_player"
 | 
				
			||||||
 | 
					            android:padding="10dp"
 | 
				
			||||||
 | 
					            app:srcCompat="@drawable/ic_baseline_arrow_back_24" />
 | 
				
			||||||
 | 
					    </LinearLayout>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <androidx.recyclerview.widget.RecyclerView
 | 
				
			||||||
 | 
					        android:id="@+id/recycler_episodes_player"
 | 
				
			||||||
 | 
					        android:layout_width="0dp"
 | 
				
			||||||
 | 
					        android:layout_height="0dp"
 | 
				
			||||||
 | 
					        android:orientation="horizontal"
 | 
				
			||||||
 | 
					        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
 | 
				
			||||||
 | 
					        app:layout_constraintBottom_toBottomOf="parent"
 | 
				
			||||||
 | 
					        app:layout_constraintEnd_toEndOf="parent"
 | 
				
			||||||
 | 
					        app:layout_constraintStart_toStartOf="parent"
 | 
				
			||||||
 | 
					        app:layout_constraintTop_toBottomOf="@+id/linearLayout3"
 | 
				
			||||||
 | 
					        tools:listitem="@layout/item_episode_player" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</androidx.constraintlayout.widget.ConstraintLayout>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user