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 c825ddd..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,8 +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.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 @@ -124,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() { @@ -151,16 +157,16 @@ class PlayerActivity : AppCompatActivity() { } if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) { - if (nextEpManually) { - nextEpManually = false - } else { + // if next episode btn was clicked, skipp playNextEpisode() on STATE_ENDED + if (!nextEpManually) { playNextEpisode() } + nextEpManually = false } } }) - playCurrentMedia(true) + playCurrentMedia(true) // start initial media } @SuppressLint("ClickableViewAccessibility") @@ -187,6 +193,7 @@ class PlayerActivity : AppCompatActivity() { ffwd_10.setOnButtonClickListener { fastForward() } button_next_ep.setOnClickListener { playNextEpisode() } button_next_ep_c.setOnClickListener { playNextEpisode() } + button_episodes.setOnClickListener { showEpisodesList() } } private fun initTimeUpdates() { @@ -292,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) } /** @@ -314,7 +314,11 @@ class PlayerActivity : AppCompatActivity() { private fun playCurrentMedia(seekToPosition: Boolean) { // update the gui 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 { model.currentEpisode.title } @@ -323,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() - "" - } } /** @@ -383,8 +373,6 @@ class PlayerActivity : AppCompatActivity() { .setListener(null) } - - /** * hide the next episode button * 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() { /** @@ -419,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/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/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt new file mode 100644 index 0000000..470a559 --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/util/adapter/PlayerEpisodeItemAdapter.kt @@ -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) : RecyclerView.Adapter() { + + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 12f0b67..7f35e23 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -1,6 +1,5 @@ + + + + + + + + + + + + + + \ No newline at end of file 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_list.xml b/app/src/main/res/layout/player_episodes_list.xml new file mode 100644 index 0000000..00f5fb9 --- /dev/null +++ b/app/src/main/res/layout/player_episodes_list.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + \ No newline at end of file