add episodes list to player [Part 2]
This commit is contained in:
		| @ -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 | ||||
|         } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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) { | ||||
|         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 } | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
		Reference in New Issue
	
	Block a user