diff --git a/app/build.gradle b/app/build.gradle index 07a76bc..daecde2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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() diff --git a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt index de0adda..b671f9f 100644 --- a/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/player/PlayerActivity.kt @@ -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 } diff --git a/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt index 196c20b..14c9803 100644 --- a/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/player/PlayerViewModel.kt @@ -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 diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt new file mode 100644 index 0000000..ef03512 --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt @@ -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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesPlayer.kt deleted file mode 100644 index ba04599..0000000 --- a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesPlayer.kt +++ /dev/null @@ -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]) - } - } - - -} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt index d9ce5f2..8b7197a 100644 --- a/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt @@ -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 } diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index eef0fb9..57cda35 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -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, diff --git a/app/src/main/res/layout/player_controls.xml b/app/src/main/res/layout/player_controls.xml index cc5d2ca..a7e424d 100644 --- a/app/src/main/res/layout/player_controls.xml +++ b/app/src/main/res/layout/player_controls.xml @@ -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" diff --git a/app/src/main/res/layout/player_episodes.xml b/app/src/main/res/layout/player_episodes_list.xml similarity index 100% rename from app/src/main/res/layout/player_episodes.xml rename to app/src/main/res/layout/player_episodes_list.xml